Skip to content

[Implementation] Backstage cataloger: AWS SigV4 auth (self-refreshing IAM-role creds)#232

Merged
me-bender[bot] merged 5 commits into
mainfrom
bender/backstage-sigv4-auth
Jul 9, 2026
Merged

[Implementation] Backstage cataloger: AWS SigV4 auth (self-refreshing IAM-role creds)#232
me-bender[bot] merged 5 commits into
mainfrom
bender/backstage-sigv4-auth

Conversation

@me-bender

@me-bender me-bender Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What this is

Spec-only PR (per the plugin playbook) adding AWS SigV4 authentication to the live-API catalogers/backstage cataloger, which today only supports Bearer tokens. For Backstage APIs fronted by AWS IAM auth (e.g. Amazon API Gateway), Bearer tokens are rejected — every request must be SigV4-signed.

Tracks ENG-1119.

What's included

  • catalogers/backstage/lunar-cataloger.yml — new auth_mode (bearer default | sigv4), aws_region, aws_service inputs; optional static-key AWS secrets; landing-page note.
  • catalogers/backstage/README.md — "AWS SigV4 Authentication" section (credential chain, one-time IAM-role setup, sidecar alternative).

No main.sh changes yet — implementation follows secondary-reviewer approval.

Design summary

The driving requirement: it must work without the user supplying new credentials constantly — so static long-lived keys and manually-refreshed session tokens are out.

In sigv4 mode the cataloger resolves AWS credentials at runtime from the standard AWS credential provider chain, re-resolved on every run, so short-lived IAM-role creds always sign fresh with nothing for the user to rotate:

  1. IRSA (EKS) — recommended. Pod runs under a role-annotated service account; web-identity token → STS → temporary creds. Token auto-rotates; each run re-exchanges.
  2. ECS task role → container credentials endpoint.
  3. EC2 instance profile → IMDSv2.
  4. Static keys (optional secrets) → escape hatch for runners with no IAM identity; does not self-refresh.

Signing itself is curl --aws-sigv4, already present in the base image (curl 8.19.0) — zero new dependencies (verified against the base image on ENG-1119: it emits a canonical AWS4-HMAC-SHA256 header with x-amz-security-token).

One-time deploy prerequisite (documented in the README): the role annotation goes on the snippet-pod service account (OPERATOR_POD_SERVICE_ACCOUNT, chart <release>-script-pod) that catalogers actually execute under — not the hub SA. Called out explicitly because annotating the hub SA is the easy mistake.

Relationship to existing plugins

Enhances the existing catalogers/backstage (live-API sync). bearer remains the default — existing installs are unaffected. No Component JSON schema change; output paths are identical.

Testing plan

  • Unit-level, no vendor access: SigV4 signing is already proven with curl --aws-sigv4 against the base image (ENG-1119). At implementation we'll add credential-chain resolution (STS AssumeRoleWithWebIdentity for IRSA is a token-authenticated POST; ECS/IMDS return JSON) and cover the resolver with fixtures/mocked endpoints.
  • Full E2E needs a Backstage instance behind AWS IAM auth, which we don't have in-house. Because of that, status stays experimental. To validate end-to-end we'd need either a test API-Gateway-fronted Backstage or coordination with a deployment that has one.

Open questions

  1. Native chain vs sidecar as the recommended path. I've led with native in-cataloger resolution (self-contained plugin) and documented the aws-sigv4-proxy sidecar as the no-plugin-code alternative. Both self-refresh. Preference?
  2. Keep the static-key escape hatch? It's optional and clearly subordinated, but it slightly widens the surface. Drop it and go role-only?
  3. aws_service default execute-api assumes API Gateway. Fine as the default?

…M-role creds)

Adds auth_mode (bearer|sigv4), aws_region, and aws_service inputs plus
optional static-key AWS secrets to the backstage cataloger manifest, and
documents SigV4 auth in the README.

In sigv4 mode the cataloger resolves AWS credentials at runtime from the
standard credential chain (IRSA / ECS task role / EC2 instance profile /
static keys) and re-resolves them each run, so short-lived IAM-role creds
self-refresh with no user-supplied secrets to rotate. curl's native
--aws-sigv4 (already in the base image) does the signing.

Spec only — no main.sh changes yet.
@me-bender me-bender Bot requested a review from brandonSc July 3, 2026 00:55
@lunar-internal

lunar-internal Bot commented Jul 3, 2026

Copy link
Copy Markdown

🌒 Earthly Lunar

✅ 14 Passing

  • branch-protection-enabled vcs.branch-protection-enabled - Requires branch protection rules to be enabled on the default branch.
    Branch protection is the foundation for all other VCS security controls.

  • build-tagged container.build-tagged - Requires container builds to use an explicit image tag via -t/--tag.
    Untagged builds produce anonymous images that cannot be tracked or deployed.

  • changelog-exists changelog.changelog-exists - Verifies that a CHANGELOG file exists in the repository root. Detects
    common variants (CHANGELOG.md, CHANGELOG, CHANGES.md, HISTORY.md,
    RELEASES.md). Intended for repos that ship versioned releases — apply
    via lunar-config on: targeting (e.g. public-only) rather than blanketly.
    Reads from .repo.changelog.

11 more...
  • codeowners-catchall repo-hygiene.codeowners-catchall - Requires a default catch-all rule (*) in CODEOWNERS so that every file
    in the repository has at least one owner.

  • codeowners-exists repo-hygiene.codeowners-exists - Requires a CODEOWNERS file to be present in the repository.
    Checks standard locations: root, .github/, or docs/.

  • codeowners-valid repo-hygiene.codeowners-valid - Validates that the CODEOWNERS file has correct syntax.
    Checks that all owner references use valid formats (@user, @org/team, or email).

  • disallow-force-push vcs.disallow-force-push - Prohibits force pushes to protected branches to preserve commit history.
    Force pushes can destroy audit trails and cause data loss.

  • instruction-file-exists ai.instruction-file-exists - Verifies that an agent instruction file exists at the repository root. Checks
    ai.instructions.all[] which is populated by the ai collector (AGENTS.md) and
    tool-specific collectors via array append (CLAUDE.md, CODEX.md, GEMINI.md).
    Passes if any entry exists.

  • max-severity sca-high.max-severity - Ensures no findings at or above the configured severity threshold.
    Configure min_severity to set the threshold (critical, high, medium, low).

  • no-latest container.no-latest - Prevents use of the :latest tag (explicit or implicit) in base images.
    Using :latest creates non-reproducible builds and makes debugging difficult.

  • readme-exists repo-hygiene.readme-exists - Verifies that a README file exists in the repository root.
    Every repository should have basic documentation for discoverability.

  • require-default-branch vcs.require-default-branch - Validates the default branch name matches the required name (default "main").
    Helps standardize branch naming across repositories.

  • require-pull-request vcs.require-pull-request - Requires all changes to go through pull requests before merging.
    Prevents direct pushes to protected branches without review.

  • stable-tags container.stable-tags - Requires base images to use stable tags: digests (sha256:...) or full semver (1.2.3).
    Partial versions like "node:20" can change unexpectedly and break builds.

More Details

@brandonSc brandonSc marked this pull request as ready for review July 3, 2026 19:03
@brandonSc brandonSc requested a review from vladaionescu as a code owner July 3, 2026 19:03

@im-fry im-fry Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read the spec. design is coherent and the files match the spec-phase playbook. two things for the primary reviewer to weigh in on inline. the sidecar 'alternative' section doesnt actually fit lunar's snippet-pod model, and the aws_region failure mode when unset in sigv4 mode isnt pinned.

Comment thread catalogers/backstage/README.md Outdated
Comment thread catalogers/backstage/lunar-cataloger.yml
…ecar) + pin aws_region failure mode

Addresses Fry review on #232:
- The proxy 'alternative' framed aws-sigv4-proxy as a same-pod sidecar at
  localhost:8080, which doesn't fit Lunar's snippet-pod model — the operator
  builds the pod's container list (snippet container, which
  OPERATOR_SNIPPET_CONTAINER_SPEC_* replaces rather than appends, + the
  built-in sidecar) with no hook to add a container. Reworked to a standalone
  Deployment + Service reached via cluster DNS, with IRSA on the proxy's own SA.
- Pinned aws_region behavior: in sigv4 mode with neither the input nor
  AWS_REGION set, the cataloger fails fast with a clear error instead of
  signing with an empty region.

@im-fry im-fry Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed cleanly. sidecar section reworked as a standalone Deployment + Service reached via cluster DNS (with the no-hook-to-inject explanation right there so no one gets bit), and the aws_region failure-mode is pinned in the manifest.

@brandonSc

Copy link
Copy Markdown
Contributor

Lets proceed to implementation but we should ideal test this somehow on cronos can you DM me instructions for how to this? Maybe doesn't have to be a real backstage but verifying the auth chain works.

Adds the sigv4 implementation to main.sh behind auth_mode (default bearer,
unchanged). In sigv4 mode it resolves AWS credentials once at startup from
the standard provider chain and signs every request with curl --aws-sigv4:

  1. static keys (LUNAR_SECRET_AWS_* or ambient AWS_* env)
  2. IRSA / web identity  -> STS AssumeRoleWithWebIdentity (token-auth POST,
     XML parsed with python3 stdlib)
  3. ECS task role        -> container credentials endpoint (JSON)
  4. EC2 instance profile -> IMDSv2

Session-token creds get the x-amz-security-token header. aws_region resolves
from the input or AWS_REGION/AWS_DEFAULT_REGION and fails fast with a clear
error if unset (per review). Zero new deps: curl 8.19 + jq + python3 are all
in base-main. Resolved once per run; each scheduled run re-resolves, so
role creds self-refresh with nothing to rotate.

Locally verified end-to-end against a header-capturing server: the sigv4
path emits a valid AWS4-HMAC-SHA256 signature; region fail-fast and the
unchanged bearer path both behave as expected.
@me-bender me-bender Bot changed the title [Spec Only] Backstage cataloger: AWS SigV4 auth (self-refreshing IAM-role creds) [Implementation] Backstage cataloger: AWS SigV4 auth (self-refreshing IAM-role creds) Jul 3, 2026
@me-bender

me-bender Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Implementation pushed (201f2c44) — main.sh now implements auth_mode: sigv4.

Credentials resolve once at startup from the standard chain (static keys → IRSA web-identity via STS AssumeRoleWithWebIdentity → ECS task role → EC2 IMDSv2) and every request is signed with curl --aws-sigv4. Zero new deps — curl 8.19 + jq + python3 are all in base-main. Resolved once per run; each scheduled run re-resolves, so role creds self-refresh with nothing to rotate.

Local test evidence

Ran the actual main.sh against a header-capturing server:

auth_mode=sigv4 + static keys → valid signature, exit 0:

Auth: SigV4 (region=us-east-1 service=execute-api, credentials via static-keys)
→ server received:
Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260703/us-east-1/execute-api/aws4_request,
  SignedHeaders=accept;host;x-amz-date, Signature=4a6bc82c…
X-Amz-Date: 20260703T193319Z

(AKIAIOSFODNN7EXAMPLE is AWS's public example key.)

auth_mode=sigv4, no region / no AWS_REGION → fails fast (exit 1):

ERROR: aws_region required for sigv4 (set the aws_region input or the AWS_REGION env var)

auth_mode=bearer (default) → unchanged, exit 0.

The static-keys path shares the exact signing + AUTH_ARGS construction with the IRSA/ECS/IMDS sources — only the credential source differs. IRSA resolution only exercises inside a real EKS pod, so that leg gets verified on cronos next (runbook in flight).

status stays experimental until exercised against a real IAM-fronted Backstage.

@im-fry im-fry Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the auth chain implementation is in shape but the ORDER inverts what the README says. static keys are #1 in the code (incl. ambient AWS_* env), but #4 in the docs as an 'escape hatch' fallback. so IRSA silently loses to any stray env var. one inline nit on the EKS Pod Identity token-file variant too.

Comment thread catalogers/backstage/main.sh Outdated
Comment thread catalogers/backstage/main.sh Outdated
…-file

Addresses Fry review on #232:
- Reorder the chain to match the README (and the plugin's self-refreshing-by-
  default promise): IRSA / EKS Pod Identity -> ECS -> EC2 IMDSv2 -> static
  LUNAR_SECRET_AWS_* keys LAST. Previously static was #1, so an attached IRSA
  role lost to any credential source ahead of it.
- Drop the ambient AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env fallback
  entirely. A stray AWS_* env var (sidecar, Secret mount, dev tooling) must
  not silently preempt an annotated role — that would break self-refresh.
  Static is now the deliberate opt-in LUNAR_SECRET_AWS_* path only.
- ECS branch now also handles EKS Pod Identity: read the rotating
  AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE (fresh at resolve time) in addition
  to the direct-value AWS_CONTAINER_AUTHORIZATION_TOKEN env.

Verified locally: static LUNAR_SECRET_* resolves + signs; a stray ambient
AWS_ACCESS_KEY_ID is now ignored (no longer preempts role sources); no-cred
case fails fast.

@im-fry im-fry Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed cleanly. chain reordered to IRSA -> container creds -> IMDS -> static (LUNAR_SECRET_AWS_* only, ambient AWS_* dropped so a stray env var cant preempt an attached role), and the container-creds branch now reads AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE for Pod Identity. matches the README ordering.

Comment thread catalogers/backstage/main.sh
Combines the SigV4 auth work with main's backstage rewrite (by-query
cursor pagination + api_path_prefix). Conflicts were doc/echo-only:
- main.sh: kept main's URL echo (includes ${API_PATH_PREFIX}) + my auth-mode line
- README.md: kept both new sections (AWS SigV4 Authentication + API Path Prefix)
resolve_aws_credentials / AUTH_ARGS auto-merged onto the by-query fetch.
Re-verified locally: sigv4 path still emits a valid AWS4-HMAC-SHA256 sig
against the by-query {"items":[]} shape and exits 0.
@me-bender me-bender Bot merged commit 65b93f6 into main Jul 9, 2026
4 checks passed
@me-bender me-bender Bot deleted the bender/backstage-sigv4-auth branch July 9, 2026 21:32
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