Skip to content

Commit 6bb148b

Browse files
authored
feat: enforce IMDSv2 by default (Phase 6.a) (#24)
Part of #12. Phase 6 split: this PR ships IMDSv2 enforcement only. EBS encryption (also scoped in #12) becomes a Phase 6.b follow-up that needs per-AMI root-device lookup to set BlockDeviceMappings safely (wrong DeviceName creates an unused extra volume or clobbers the root). ## Change RunInstances params now include: MetadataOptions: { HttpTokens: config.input.httpTokens, HttpPutResponseHopLimit: 1, HttpEndpoint: 'enabled', } - HttpTokens default 'required' — IMDSv2 session token mandatory on every metadata request. Mitigates SSRF to IAM-credential theft. - HttpPutResponseHopLimit: 1 — token can't cross into a nested container, cap the blast radius of a containerized workload that tries to walk metadata. - HttpEndpoint: 'enabled' — explicit; default but good to pin. ## New input 'http-tokens' in action.yml, default 'required', accepted values 'required' and 'optional'. Consumers who must keep IMDSv1 compatibility set 'optional' explicitly. ## Tests tests/config.test.js — 2 new cases: default fallback + override. Total 34 -> 36. ## Consumer impact Transparent for code running on the runner that uses IMDS via a modern SDK (aws-sdk v2/v3, SSM agent, cloud-init) — all support IMDSv2 without config. Risk: an old tool or hand-rolled script that hits 169.254.169.254/latest/meta-data without first PUTting for a token. None expected on the hardened AL2023 AMI. ## Dogfood Small single-knob change — should pass the provider acctest cleanly once rotated. Phase 6.b (EBS encryption) will be scoped separately. Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
1 parent 46cf1d0 commit 6bb148b

5 files changed

Lines changed: 43 additions & 0 deletions

File tree

action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ inputs:
7070
IAM Role Name to attach to the created EC2 instance.
7171
This requires additional permissions on the AWS role used to launch instances.
7272
required: false
73+
http-tokens:
74+
description: >-
75+
Instance Metadata Service (IMDS) token mode. Accepted values:
76+
- 'required' (default): IMDSv2-only. Any request to the IMDS
77+
endpoint (169.254.169.254) must present a session token.
78+
Mitigates SSRF-style credential theft.
79+
- 'optional': IMDSv1 and IMDSv2 both work. Only set this if
80+
a consumer workflow explicitly needs IMDSv1 compatibility.
81+
Passed through to RunInstances MetadataOptions.HttpTokens.
82+
required: false
83+
default: 'required'
7384
debug:
7485
description: >-
7586
When 'true', the action emits extra diagnostic output to the

dist/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87942,6 +87942,15 @@ async function startEc2Instance(label, githubRegistrationToken) {
8794287942
SecurityGroupIds: [config.input.securityGroupId],
8794387943
IamInstanceProfile: { Name: config.input.iamRoleName },
8794487944
TagSpecifications: config.tagSpecifications,
87945+
// IMDSv2 required by default. Mitigates SSRF-style IAM credential
87946+
// theft from the runner — any metadata request must present a
87947+
// session token. HttpPutResponseHopLimit: 1 prevents the token
87948+
// from reaching containerized workloads one hop deeper.
87949+
MetadataOptions: {
87950+
HttpTokens: config.input.httpTokens,
87951+
HttpPutResponseHopLimit: 1,
87952+
HttpEndpoint: 'enabled',
87953+
},
8794587954
};
8794687955

8794787956
let ec2InstanceId;
@@ -88033,6 +88042,7 @@ class Config {
8803388042
label: core.getInput('label'),
8803488043
ec2InstanceId: core.getInput('ec2-instance-id'),
8803588044
iamRoleName: core.getInput('iam-role-name'),
88045+
httpTokens: core.getInput('http-tokens') || 'required',
8803688046
debug: core.getInput('debug') || 'false',
8803788047
};
8803888048

src/aws.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ async function startEc2Instance(label, githubRegistrationToken) {
9696
SecurityGroupIds: [config.input.securityGroupId],
9797
IamInstanceProfile: { Name: config.input.iamRoleName },
9898
TagSpecifications: config.tagSpecifications,
99+
// IMDSv2 required by default. Mitigates SSRF-style IAM credential
100+
// theft from the runner — any metadata request must present a
101+
// session token. HttpPutResponseHopLimit: 1 prevents the token
102+
// from reaching containerized workloads one hop deeper.
103+
MetadataOptions: {
104+
HttpTokens: config.input.httpTokens,
105+
HttpPutResponseHopLimit: 1,
106+
HttpEndpoint: 'enabled',
107+
},
99108
};
100109

101110
let ec2InstanceId;

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Config {
1616
label: core.getInput('label'),
1717
ec2InstanceId: core.getInput('ec2-instance-id'),
1818
iamRoleName: core.getInput('iam-role-name'),
19+
httpTokens: core.getInput('http-tokens') || 'required',
1920
debug: core.getInput('debug') || 'false',
2021
};
2122

tests/config.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ describe('Config — mode validation', () => {
131131
});
132132
});
133133

134+
describe('Config — http-tokens input', () => {
135+
test('defaults to "required" when unset', () => {
136+
const config = loadConfig(startModeInputs);
137+
expect(config.input.httpTokens).toBe('required');
138+
});
139+
140+
test('honors an "optional" override', () => {
141+
const config = loadConfig({ ...startModeInputs, 'http-tokens': 'optional' });
142+
expect(config.input.httpTokens).toBe('optional');
143+
});
144+
});
145+
134146
describe('Config — debug input', () => {
135147
test('defaults to "false" when unset', () => {
136148
const config = loadConfig(startModeInputs);

0 commit comments

Comments
 (0)