Skip to content

feat: enforce IMDSv2 by default (Phase 6.a)#24

Merged
kurok merged 1 commit into
feat/al2023-supportfrom
feat/12-imdsv2
Apr 21, 2026
Merged

feat: enforce IMDSv2 by default (Phase 6.a)#24
kurok merged 1 commit into
feat/al2023-supportfrom
feat/12-imdsv2

Conversation

@kurok
Copy link
Copy Markdown

@kurok kurok commented Apr 21, 2026

Part of #12. Phase 6 split into two — this PR lands IMDSv2 enforcement only.

EBS encryption (also scoped in #12) is pushed to a Phase 6.b follow-up: setting BlockDeviceMappings[*].Ebs.Encrypted: true requires knowing the AMI's root device name (/dev/xvda vs /dev/sda1 etc.) to avoid creating an unused extra volume or clobbering the root. Safer to land the one-line-per-API-call IMDSv2 change independently.

Change

RunInstances params now include:

MetadataOptions: {
  HttpTokens: config.input.httpTokens,
  HttpPutResponseHopLimit: 1,
  HttpEndpoint: 'enabled',
}

Rationale per line:

  • HttpTokens: 'required' (default via new input) — IMDSv2 session token mandatory. Mitigates the SSRF-to-IAM-credential-theft class of bug.
  • HttpPutResponseHopLimit: 1 — the token cannot cross into a nested container hop. Bounds the blast radius if a containerized workload on the runner tries to walk metadata.
  • HttpEndpoint: 'enabled' — explicit; matches default but pins the value so a future AWS default-flip doesn't silently disable metadata.

New input

http-tokens in action.yml, default 'required', accepted values 'required' / 'optional'. Consumers who must keep IMDSv1 compatibility for a specific workload set 'optional' explicitly.

Tests

tests/config.test.js — 2 new cases: default fallback + explicit override. Total 34 → 36.

Compatibility

Transparent for code using IMDS via any modern SDK (aws-sdk v2/v3, SSM agent, cloud-init) — all support IMDSv2 natively. Risk: a hand-rolled script or an old tool that hits 169.254.169.254/latest/meta-data without first PUTting for a token.

None of that exists in the provider's acctest path — make testacc is plain go test and doesn't touch IMDS.

Follow-up

  • Phase 6.b issue for EBS encryption (will query the AMI's root device name first).

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>
@kurok kurok merged commit 6bb148b into feat/al2023-support Apr 21, 2026
4 checks passed
@kurok kurok deleted the feat/12-imdsv2 branch April 21, 2026 10:26
kurok added a commit to namecheap/terraform-provider-namecheap that referenced this pull request Apr 21, 2026
…187)

namecheap/ec2-github-runner#24 merged. RunInstances now sets
MetadataOptions.HttpTokens to 'required' by default (opt-out via
new http-tokens input). Mitigates SSRF-to-IAM-credential theft
from workloads running on the runner. Transparent for aws-sdk,
SSM agent, cloud-init, and any go/python workflow — all use
IMDSv2 natively.

Rotation: 46cf1d0 (Phase 5) -> 6bb148b (Phase 6.a).

Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
kurok added a commit that referenced this pull request Apr 21, 2026
Rounds out Phase 6 (IMDSv2 landed in #24, EBS encryption deferred
until a per-AMI root-device lookup could be done safely).

## Change

New 'encrypt-ebs' input on action.yml, default 'false' (opt-in).
When 'true', the action:

1. Fetches the AMI's DescribeImages result (already needed to
   resolve image IDs when 'ec2-image-filters' is set).
2. Finds the BlockDeviceMapping matching the AMI's RootDeviceName.
3. Clones that mapping, drops SnapshotId (AWS uses the AMI's
   snapshot automatically), sets 'Encrypted: true'.
4. Passes the cloned mapping as RunInstances.BlockDeviceMappings.

Result: root volume launches with SSE-EBS, key 'alias/aws/ebs' in
the launch account. VolumeSize / VolumeType / IOPS / DeleteOnTermination
preserved from the AMI — only the encryption bit is new.

## Why opt-in

The launch account (not necessarily the AMI owner account) must
have either default EBS encryption enabled, or at minimum permission
to use the default AWS-managed KMS key. If the AMI's snapshot is
encrypted with a customer-managed key that doesn't have a cross-
account grant, RunInstances fails. Defaulting to 'true' would
regress every consumer whose IAM / KMS policy isn't set up for
this. Default 'false' lets each consumer opt in after verifying
their account can handle it.

## Why not account-level default encryption

AWS supports 'aws ec2 enable-ebs-encryption-by-default' at the
account level — and that's the preferred belt-and-suspenders. But
not every consumer runs in an AWS account they control (e.g.,
Namecheap's CI runs in a shared org account). Action-side opt-in
is the only portable control.

## Refactor alongside

resolveImageId -> resolveImage: now returns both the ID and the
full Image metadata. Callers that only need the ID use .id; the
EBS-encryption code path uses .image.BlockDeviceMappings to build
the encrypted clone.

## Tests

tests/ebs.test.js — 6 new cases for buildEncryptedRootMapping:
happy path with full EBS config + non-EBS sibling mapping, volume
type / size / iops preservation, and five null-return paths for
exotic AMI shapes (no RootDeviceName, no mappings, non-EBS root,
orphan RootDeviceName).

tests/config.test.js — 2 new cases for the encrypt-ebs input
(default fallback + override).

Total: 44 -> 52 tests.

## Consumer dogfood

Separate PR on terraform-provider-namecheap rotates the pin and
enables 'encrypt-ebs: true' on the CI job. If the dogfood fails
with a KMS / IAM permissions error, we know the account needs
policy work before enabling; action-side code is fine either way.

Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant