@@ -115,3 +115,213 @@ cancels an in-progress deployment.
115115GitHub Actions and GHCR have account-specific included usage and storage limits.
116116Monitor the account's current quotas; immutable commit tags and rollback images
117117are intentionally retained rather than automatically deleted.
118+
119+ ## Temporary GitHub Actions SSH access with AWS OIDC
120+
121+ The EC2 security group sg-05c1462a133b8d00f is managed by a dedicated
122+ GitHub Actions role. For deploy and rollback runs, the workflow obtains a
123+ short-lived AWS session through GitHub OIDC, reads the current runner IPv4 from
124+ https://checkip.amazonaws.com/ over verified HTTPS, validates that the
125+ response is exactly one globally routable IPv4 address, and adds one TCP/22
126+ /32 ingress rule. The current fixed rule TTL is two hours. The helper stores
127+ the AWS-returned sgr-... rule ID as a job output and passes that exact ID to
128+ the cleanup command. It never searches by or revokes the Mac rule
129+ sgr-070a3d2767818f890; the existing Mac, HTTP, and HTTPS rules are otherwise
130+ left alone.
131+
132+ The revoke helper first describes the requested rule in the fixed security
133+ group and checks its group, rule ID, direction, TCP/22 ports, public /32 source,
134+ management tags, description, and this workflow run identity. It then calls
135+ revoke-security-group-ingress with that one AWS rule ID and verifies the rule
136+ is absent. Errors are reduced to fixed messages. A failed revoke fails the job.
137+ The workflow's always() cleanup step handles normal failures and cancellation.
138+ A scheduled job runs every 15 minutes in the separate
139+ production-ssh-recovery GitHub environment and removes only expired rules with
140+ the exact CodeLens owner/run/expiry tags and matching run description. The
141+ recovery environment must be restricted
142+ to main, have no secrets, and have no reviewer gate so cleanup can run
143+ unattended. It receives the same narrowly scoped AWS role. A manual recover_ssh
144+ dispatch from main is also available.
145+
146+ ### GitHub setup
147+
148+ 1 . Keep the existing protected production environment and its required
149+ reviewers. Its deployment branches must remain restricted to main.
150+ 2 . Create production-ssh-recovery. Restrict its deployment branches to main;
151+ add no secrets and no required reviewers. It is used only by the 15-minute
152+ stale-rule reaper and manual recover_ssh operation.
153+ 3 . Add repository variable CODELENS_AWS_ROLE_ARN with the ARN of the IAM role
154+ described below. It is an ARN, not a credential. Do not add AWS access keys.
155+ 4 . Keep EC2_HOST, EC2_USER, EC2_SSH_PRIVATE_KEY, and EC2_KNOWN_HOSTS secrets
156+ only in production. No EC2 SSH secret is used by the recovery job.
157+ 5 . The IAM OIDC provider URL is
158+ https://token.actions.githubusercontent.com , with audience sts.amazonaws.com.
159+ Add the provider only if it does not already exist.
160+
161+ The verified GitHub repository OIDC settings are ` use_default: true ` ,
162+ ` use_immutable_subject: true ` , and immutable subject prefix
163+ ` repo@99238925/CodeLens@1334470875 ` . No custom include-claim-key list is
164+ configured. With an environment, the exact subject values are
165+ ` repo:sprahasingh@99238925/CodeLens@1334470875:environment:production ` and
166+ ` repo:sprahasingh@99238925/CodeLens@1334470875:environment:production-ssh-recovery ` .
167+ GitHub documents this immutable ` repo:OWNER@OWNER-ID/REPO@REPO-ID ` syntax and
168+ the ` :environment:ENVIRONMENT ` suffix for environment jobs. The trust policy
169+ below accepts only those two subjects, the expected audience, repository and
170+ owner IDs, and ` main ` .
171+
172+ ### IAM role trust policy
173+
174+ Create the IAM role ` CodeLensGitHubActionsSSH ` and use this trust policy.
175+ The account and GitHub IDs below are the verified values. The OIDC provider
176+ must exist in AWS account ` 510155707736 ` , region ` eu-north-1 ` .
177+
178+ {
179+ "Version": "2012-10-17",
180+ "Statement": [
181+ {
182+ "Effect": "Allow",
183+ "Principal": {
184+ "Federated": "arn:aws:iam::510155707736:oidc-provider/token.actions.githubusercontent.com"
185+ },
186+ "Action": "sts:AssumeRoleWithWebIdentity",
187+ "Condition": {
188+ "StringEquals": {
189+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
190+ "token.actions.githubusercontent.com:repository_owner_id": "99238925",
191+ "token.actions.githubusercontent.com:repository_id": "1334470875",
192+ "token.actions.githubusercontent.com:ref": "refs/heads/main",
193+ "token.actions.githubusercontent.com:environment": [
194+ "production",
195+ "production-ssh-recovery"
196+ ],
197+ "token.actions.githubusercontent.com:sub": [
198+ "repo:sprahasingh@99238925/CodeLens@1334470875:environment:production",
199+ "repo:sprahasingh@99238925/CodeLens@1334470875:environment:production-ssh-recovery"
200+ ]
201+ }
202+ }
203+ }
204+ ]
205+ }
206+
207+ ### IAM permissions policy
208+
209+ Attach only this inline policy to that role. The helper authorizes rules with
210+ ` TagSpecifications ` on the ` AuthorizeSecurityGroupIngress ` API call; it does
211+ not make a separate ` CreateTags ` API call. EC2 performs dependent authorization
212+ for ` ec2:CreateTags ` when tags are supplied during rule creation. This policy
213+ allows that tag-on-create path only, limits ingress authorization and
214+ revocation to the CodeLens security group, and permits read-only rule
215+ inspection in ` eu-north-1 ` . It grants no instance, key, network-interface, or
216+ other security-group management permissions.
217+
218+ {
219+ "Version": "2012-10-17",
220+ "Statement": [
221+ {
222+ "Sid": "DescribeSecurityGroupRules",
223+ "Effect": "Allow",
224+ "Action": "ec2:DescribeSecurityGroupRules",
225+ "Resource": "*",
226+ "Condition": {
227+ "StringEquals": {"ec2:Region": "eu-north-1"}
228+ }
229+ },
230+ {
231+ "Sid": "AuthorizeIngressOnCodeLensGroup",
232+ "Effect": "Allow",
233+ "Action": "ec2:AuthorizeSecurityGroupIngress",
234+ "Resource": "arn:aws:ec2:eu-north-1:510155707736:security-group/sg-05c1462a133b8d00f",
235+ "Condition": {
236+ "StringEquals": {
237+ "ec2:SecurityGroupID": "sg-05c1462a133b8d00f",
238+ "ec2:Region": "eu-north-1"
239+ }
240+ }
241+ },
242+ {
243+ "Sid": "AuthorizeTaggedRuleCreation",
244+ "Effect": "Allow",
245+ "Action": "ec2:AuthorizeSecurityGroupIngress",
246+ "Resource": "arn:aws:ec2:eu-north-1:510155707736:security-group-rule/*",
247+ "Condition": {
248+ "StringEquals": {
249+ "aws:RequestTag/CodeLensManagedBy": "CodeLensGitHubActions",
250+ "ec2:Region": "eu-north-1"
251+ },
252+ "ForAllValues:StringEquals": {
253+ "aws:TagKeys": ["CodeLensManagedBy", "CodeLensRun", "CodeLensExpiresAt"]
254+ },
255+ "Null": {
256+ "aws:RequestTag/CodeLensRun": "false",
257+ "aws:RequestTag/CodeLensExpiresAt": "false"
258+ }
259+ }
260+ },
261+ {
262+ "Sid": "TagOnlyRulesCreatedByIngressAuthorization",
263+ "Effect": "Allow",
264+ "Action": "ec2:CreateTags",
265+ "Resource": "arn:aws:ec2:eu-north-1:510155707736:security-group-rule/*",
266+ "Condition": {
267+ "StringEquals": {
268+ "ec2:CreateAction": "AuthorizeSecurityGroupIngress",
269+ "aws:RequestTag/CodeLensManagedBy": "CodeLensGitHubActions",
270+ "ec2:Region": "eu-north-1"
271+ },
272+ "ForAllValues:StringEquals": {
273+ "aws:TagKeys": ["CodeLensManagedBy", "CodeLensRun", "CodeLensExpiresAt"]
274+ },
275+ "Null": {
276+ "aws:RequestTag/CodeLensRun": "false",
277+ "aws:RequestTag/CodeLensExpiresAt": "false"
278+ }
279+ }
280+ },
281+ {
282+ "Sid": "RevokeIngressOnlyOnCodeLensGroup",
283+ "Effect": "Allow",
284+ "Action": "ec2:RevokeSecurityGroupIngress",
285+ "Resource": "arn:aws:ec2:eu-north-1:510155707736:security-group/sg-05c1462a133b8d00f",
286+ "Condition": {
287+ "StringEquals": {
288+ "ec2:SecurityGroupID": "sg-05c1462a133b8d00f",
289+ "ec2:Region": "eu-north-1"
290+ }
291+ }
292+ }
293+ ]
294+ }
295+
296+ AWS authorizes ingress authorization and revocation at the security-group
297+ level; it does not let IAM constrain a request to TCP/22, a /32, or one specific
298+ rule ID. ` ec2:SecurityGroupID ` is applied only to the security-group resource
299+ authorization check; AWS lists request-tag keys for the separate
300+ security-group-rule resource check. The helper enforces the port, /32, and
301+ rule-ownership checks, revokes by exact rule ID, and explicitly protects both
302+ the known Mac rule ID and its current /32 source. A compromised workflow role
303+ could still add another ingress rule or revoke another rule in this one group;
304+ isolating runner SSH rules in a dedicated security group would remove that
305+ IAM-level limitation but requires a separately approved infrastructure
306+ change. ` DescribeSecurityGroupRules ` has no resource-level ARN, so its read
307+ permission uses ` Resource: "*" ` ; the region condition is the narrowest IAM
308+ scope available, while the helper filters requests to the CodeLens group.
309+
310+ ### Retry and recovery
311+
312+ After configuring the role, provider, repository variable, and recovery
313+ environment, rerun Actions → Test, publish, and deploy CodeLens → Run workflow
314+ from main with operation=deploy_existing and the same verified publication
315+ run ID, source commit, and image digest. Do not select deploy; that mode
316+ publishes a new image. The workflow still runs tests, validates the previous
317+ successful publication, waits for production approval, then applies the
318+ existing Celery idle gate and deployment-helper rollback protections.
319+
320+ On the next scheduled recovery run, expired managed rules are removed. If
321+ cleanup fails, inspect the sanitized recovery result and manually dispatch
322+ recover_ssh from main; do not remove a rule by CIDR or description. If an
323+ expired managed rule cannot pass validation, the reaper fails closed and
324+ requires manual inspection of that exact tagged rule. No automated process
325+ can guarantee cleanup during a prolonged GitHub Actions outage; the two-hour
326+ expiry marker and scheduled reaper bound expected exposure when Actions is
327+ available.
0 commit comments