Skip to content

Move the operated ttl-reaper off CloudFormation onto pkg/reaperdeploy (split from #654) #769

Open

Description

@scttfrdmn

Split out of #654, which closed when its remaining scope landed in #768.

What

The spore.host-operated ttl-reaper is deployed by CloudFormation/SAM. The
self-hosted one (spawn reaper deploy) is deployed by direct API calls through
pkg/reaperdeploy. #654 preferred consolidating the former onto the latter, and
#768 deliberately did not do it — it stamped the version instead, because that is
what made the skew visible, which was the actual complaint.

Why it is still worth doing

Why it is not urgent

The thing that hurt was not having two paths — it was that one of them was
unreadable. That is fixed: make lambda-versions-deployed now reports every
operated function's spawn:version, and CI gates that each deploy mechanism
stamps one (#768).

Related

Activity

  1. scttfrdmn commented on Oct 9, 2026

    @scttfrdmn
    ContributorAuthor

    Recommendation: do not do this as specified. My premise above was wrong.

    I wrote "pkg/reaperdeploy already has the pieces". It has them for the
    self-hosted reaper, which is architecturally the opposite of the operated
    one. Measured against the live functions today:

    operated (CFN) self-hosted (pkg/reaperdeploy)
    mode REAPER_SCAN_SELF=false — cross-account hub REAPER_SCAN_SELF: "true", hardcoded
    cross-account REAPER_ROLE_ARNS → 435415984226 no sts:AssumeRole grant at all
    env vars 13 3 (SCAN_SELF, DRY_RUN, REGIONS)
    observability 6 alarms, 4 metric filters, 1 log group none
    config surface 19 template parameters 7-field Options

    The blocker is the IAM shape, not the line count

    pkg/reaperdeploy's only sts:AssumeRole is in the trust policy — who may
    assume the Lambda's role. There is no grant, so it cannot reach another account
    at all.

    That absence is not an oversight. It is the property that made the in-account
    reaper acceptable in 942542972736, whose organization forbids trusting an
    external principal, and #613's audit recorded it explicitly: "The inline policy
    contains no sts:AssumeRole at all."

    So migrating the operated reaper onto this path means either:

    • adding an AssumeRole grant to the shared code — weakening the exact
      property that audit turned on; or
    • forking the policy by deployment shape — which recreates two paths and
      defeats the one stated benefit.

    Both cited motivations have cheaper fixes

    The trap is real — confirmed today, the dev-account role trusts
    arn:aws:iam::966362334030:role/spawn-ttl-reaper-production-TTLReaperFunctionRole-ZJ84YZ2dCPei
    with sts:ExternalId=spawn-ttl-reaper. But note what that means: changing the
    role name replaces the role and breaks every onboarded account until each one's
    trust policy is updated.
    It is a coordinated migration in its own right, not a
    one-line template edit, and it is the thing #476 already describes.

    What the migration would actually cost

    Reimplementing 6 CloudWatch alarms and 4 metric filters as imperative Go API
    calls, on the one piece of infrastructure whose failure mode is instances not
    being terminated
    . That trades declarative diffs, drift detection and automatic
    rollback for hand-rolled reconciliation — and CloudFormation is genuinely good at
    exactly this part.

    Suggested re-scope

    1. Close this, or narrow it to the RoleName fix — which addresses ttl-reaper: cross-account trust names a CFN-generated role ARN suffix — recreating the stack breaks EVERY account at once (#457 trap 1) #476, the
      concrete trap, and needs a coordinated trust-policy update across onboarded
      accounts. That is worth doing and does not require leaving CFN.
    2. Keep the operated reaper on CloudFormation. The hazard that hurt was that
      its deployed state was unreadable (spored and the lambdas never upgrade with the CLI, and nothing warns about the skew #654), and that is fixed: make lambda-versions-deployed reports its spawn:version, and test(lambda): gate that every template parameter is actually passed #779 gates that
      every parameter is passed.
    3. Leave pkg/reaperdeploy as the in-account path, with no AssumeRole grant.
      Two deployment shapes exist because there are genuinely two situations: a
      cross-account hub, and an account that may not trust one.

    Happy to do (1) if wanted — but it touches every onboarded account's trust
    policy, so it needs sequencing rather than a deploy.

  2. scttfrdmn commented on Oct 9, 2026

    @scttfrdmn
    ContributorAuthor

    Reframed by #780 — the premise matters more than the mechanics

    The project's actual architecture has three deployment shapes, and I was assessing this against two:

    1. shared — anyone uses spore.host's infra-account reaper cross-account (the default; fails when an org forbids external trust)
    2. in-account — spawn reaper deploy (No supported way to run the TTL reaper in your own account, so governed orgs can't have reaper coverage at all #625), with no sts:AssumeRole grant by design, for orgs that block exactly that
    3. own infra account — an institution deploys their own spore.host infra account, where cross-account trust IS fine because both accounts are theirs

    Shape 3 is unsupported and was untracked. Now #780.

    That changes what this issue is. Assessed as a cleanup — "one path instead of two" — it is a bad trade, for the reasons in my comment above: it means either adding an AssumeRole grant to the shape whose security story is the absence of one, or forking the policy and keeping two paths regardless.

    Assessed as enabling work for shape 3, it is a different proposition. Shape 3 requires a self-deployable cross-account hub, so pkg/reaperdeploy needs a deliberate hub mode whether or not spore.host's own reaper ever migrates. The migration then becomes a consequence — spore.host dogfooding the path it ships to institutions — rather than the goal.

    And it changes what gets built: a mode, chosen explicitly, not a widening of the default.

    The constraint that has to hold

    An in-account deploy must remain auditably free of sts:AssumeRole. That absence is what made shape 2 acceptable in a governed org, and #613's audit is quotable because the answer was "zero". If hub mode lands, it needs a test asserting the in-account path emits no AssumeRole grant — otherwise the property degrades from a fact to a claim, and the next audit cannot repeat it.

    Suggested disposition

    Keep this open but blocked on #780's design, rather than doing it as a refactor. The two things worth doing independently of either:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions