Skip to content

orphans: unassociated EIPs are reported unconditionally (no spawn/principal attribution), and both remediation hints are wrong — one is destructive #500

Description

@scttfrdmn

Summary

spawn orphans reports unassociated Elastic IPs with no ownership test at all, then tells the operator to remove them via two different mechanisms, both of which are wrong: one is a no-op, the other irreversibly deletes a resource spawn never created.

In a shared account this is an invitation to delete another project's static address. I hit it during a DFT pilot campaign in a multi-project account: the single EIP orphans reported and offered to clean up is the control-node address of an unrelated CloudFormation stack.

This is a follow-up to the EIP-visibility work in #262 / #273. #273 got the invariant exactly right — "spawn releases only what it alone creates … EIP handling here is therefore reporting-only" — and correctly hardened RemoveResource to refuse addresses. The gap is upstream of that: the scan that decides what to report has no attribution check for the unassociated case, and the user-facing text contradicts the invariant the code enforces.

Reproduction

spawn 0.100.0, commit 6521c239e5f246e4ddb476e5df67e4e58df241e5, build 2026-08-11T18:51:00Z, us-east-1.

$ spawn orphans --region us-east-1
REGION     SERVICE  TYPE     ID                          STATE         CREATED
us-east-1  ec2      address  eipalloc-002da3736d205bc22  unassociated  -

1 resource(s).

Run 'spawn cleanup' to remove these (running instances are never removed).

The reported address is not spawn's, is not mine, and carries no spawn:managed tag:

$ aws ec2 describe-addresses --allocation-ids eipalloc-002da3736d205bc22 \
    --region us-east-1 --query 'Addresses[0].{ip:PublicIp,tags:Tags}'
{
  "ip": "34.224.250.21",
  "tags": [
    {"Key": "elastic-gcs:role",                   "Value": "control"},
    {"Key": "elastic-gcs:region",                 "Value": "us-east-1"},
    {"Key": "Name",                               "Value": "elastic-gcs-control-us-east-1"},
    {"Key": "aws:cloudformation:logical-id",      "Value": "ControlNodeEIP"},
    {"Key": "aws:cloudformation:stack-name",      "Value": "ElasticGCS-Control-us-east-1"},
    {"Key": "aws:cloudformation:stack-id",        "Value": "arn:aws:cloudformation:...:stack/ElasticGCS-Control-us-east-1/..."}
  ]
}

It is a CloudFormation-managed ControlNodeEIP. Releasing it would break that stack and drift it, since CloudFormation still believes it owns the allocation.

Bug 1 — the "only yours" default does not apply to addresses

--all is documented as widening from "only yours" to "every principal in the account". For this address the two outputs are byte-identical:

$ spawn orphans --region us-east-1        # default: "only yours"
us-east-1  ec2      address  eipalloc-002da3736d205bc22  unassociated  -
$ spawn orphans --region us-east-1 --all  # "every principal"
us-east-1  ec2      address  eipalloc-002da3736d205bc22  unassociated  -

So the address row is unconditional. That is understandable — an EIP allocation has no creator attribution to filter on — but it means the narrowest invocation of orphans still reports a resource belonging to an unrelated stack, with nothing in the output to indicate that.

Note the asymmetry with #273's own reasoning. For an attached EIP, the association with a spawn instance is real ownership evidence, and #273's logic is sound. For an unassociated EIP there is no association left to test, so the same code path reports it with the ownership question silently unanswered.

spawn resources has the same issue, and is arguably worse: cleanup --help describes the resource set as "the shared AWS resources spore.host created (tagged spawn:managed)", but this untagged third-party address is listed in resources alongside genuinely spawn-managed instances and volumes.

Bug 2 — the orphans footer names a command that cannot act on addresses

The footer says:

Run 'spawn cleanup' to remove these (running instances are never removed).

Per #273 Part 2, cleanup filters addresses out by design. When the report contains only addresses — exactly my case — the footer's advice is a guaranteed no-op. An operator who follows it sees cleanup do nothing and reasonably concludes the tool is broken, or escalates to something that isn't a no-op.

Bug 3 — orphans --help asserts ownership the scan never established

spawn never allocates them, so it never releases them. Any EIP listed is yours to release with 'aws ec2 release-address'

The first clause is the correct invariant. The second does not follow from it: "spawn didn't create it" does not imply "you did." The help text converts an absence of attribution into a positive ownership claim, and then recommends the one AWS operation on an EIP that cannot be undone. This is the sentence that would have caused the damage — RemoveResource's refusal to release addresses is bypassed entirely, because the tool has routed the operator around its own guardrail and out to the raw CLI.

Impact

orphans is the safety net (#262: "at minimum, warn"). Here it warns about someone else's resource and hands over a destructive command with an unearned assurance of ownership. The failure is not that spawn deletes anything — #273 made sure it can't — it is that the text tells a human to do what the code correctly refuses to do.

Suggested fix

  1. Attribute before reporting. For unassociated EIPs, treat presence of foreign ownership markers as disqualifying — at minimum aws:cloudformation:stack-id (and ideally any aws:* reserved-prefix tag, since those are written by AWS-managed provisioners, not by hand). Either drop them from orphans or move them to a clearly separate advisory section.
  2. Show the evidence. Add a tag/owner column, or annotate the row (unassociated (CloudFormation: ElasticGCS-Control-us-east-1)). The operator needs the attribution in the report; requiring a manual describe-addresses to find out whether the safety net is pointing at a real problem defeats it.
  3. Fix the footer conditionally. Do not print "Run 'spawn cleanup'" when the report contains no cleanup-eligible resources.
  4. Rewrite the help sentence. Something like: "spawn never allocates Elastic IPs, so it never releases them. Any EIP listed here is outside spawn's control — verify its owner before acting, as release-address is irreversible." Drop the word "yours".

Point 4 is the one that matters most; 1–3 are defence in depth.

Workaround

Verified the tags manually and left the address alone. Recorded in the campaign's teardown log that the one resource spawn orphans reports is deliberately not mine and must not be released — a note that has to be re-made by hand on every teardown, because the tool asserts the opposite.

Related: #262 (EIP billing on stopped instances), #273 (added EIP visibility, established the reporting-only invariant).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions