feat(dns): SES inbound for one allowlisted agent mailbox #961
No reviewers
Labels
No labels
burndown-2026-06
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/infrastructure!961
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/wz68"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reopens the receiving half of
coilysiren.me's no-mail posture so a forge that proves ownership by mailed code can reach the agent's commit identity.Why
Tangled attributes commits by matching the author email to a verified email on an account (
GetEmailToDid, always called withisVerifiedFilter = true). Its only verification path issendVerificationEmail->CheckValidVerificationCode, with no DNS-TXT alternative, socoilyco-ops@coilysiren.mehas to genuinely receive mail.Decision trail: coilysiren/inbox#440. The agent-identity cleanup this unblocks: coilysiren/inbox#466.
What changes
0 .) to10 inbound-smtp.us-east-1.amazonaws.com, TTL 3600 to 300 so restoring0 .propagates in minutes._amazonsesTXT token.coilysiren-inbound, activated, with one rule whoserecipientslist is exactly["coilyco-ops@coilysiren.me"].coilysiren-inbound-mailbucket, 30-day expiry, SES-scoped put policy.local.all_s3_bucketsso the new bucket is not under-reported.What does not change
SPF stays
v=spf1 -alland DMARC staysp=reject; sp=reject. The domain still sends no mail and still cannot be spoofed. Only the receiving leg moved.The claim this rests on, unverified
A recipient matching no rule in the active rule set should be rejected by SES rather than accepted and discarded, which is what bounds the domain to accepting exactly one address. That is the whole basis for reopening the MX, so it gets an empirical check after apply: send to a random address at the domain and confirm a bounce.
Not yet planned
terraform planhas not run against this. The SSO refresh token for theadminprofile is expired, so Terraform's SDK cannot initialise the S3 backend even though the AWS CLI still has usable cached credentials. Needsaws sso login --profile adminfirst.Do not merge before a clean plan.
Plan is clean. Apply attempted and aborted. Nothing landed.
Plan, run 2026-08-27, nine creates and one in-place update, no destroys and no replacements:
Apply was killed at a two-minute timeout, during the refresh phase. The
aws_ssm_parameters_by_pathdata source scans/recursively, which is slow enough to exhaust the window before Terraform reaches any resource action.Verified nothing was applied, checked against AWS rather than inferred from the aborted output:
dig MX coilysiren.mestill returns0 ._amazonses.coilysiren.mereturns nothingaws ses list-identitiesemptyaws ses list-receipt-rule-setsemptyhead-bucket coilysiren-inbound-mailreturns 404One stale lock remains, from this run:
Clearing it needs
force-unlock, which the agent permission classifier blocks. Handed to Kai rather than worked around by deleting the S3 lock object, since that would be the same destructive action by another route.Unrelated drift this plan surfaced
The
inventoryoutput is missing a large batch of SSM parameters, including theteable/*,ward-mcp/*,steam-mcp/*, and severaloauth2-proxy/cookie-secretentries. That predates this branch and is output text only, driven by a data source, so no resource is affected. Applying this PR will refresh it as a side effect.Next
coilysiren.meand confirm SES rejects rather than silently accepts. That is the claim the whole MX reopening rests on, and it is still unverified.Correction: the apply did not time out on a slow refresh. It hung on an approval prompt, by design.
The previous comment blamed the two-minute timeout on
aws_ssm_parameters_by_pathscanning/recursively. That was a guess and it is wrong.scripts/_lib.py::terraform_runtakes the action fromsys.argv[1]and drops every argument after it:terraform_aws_inventory.pycallsterraform_run("aws-inventory")with the defaultauto_approve=False, deliberately. Its own docstring says why:So
just terraform-aws-inventory apply -auto-approveran plainterraform apply, which sat waiting for a typedyesagainst a shell with no TTY until it was killed. The-auto-approvenever reached terraform.This is a guardrail working exactly as intended. This module requires an interactive human apply, and that is the correct answer for a stack that owns live DNS. Recording it rather than engineering around it: the apply belongs to Kai, not to an agent running headless.
The same argument-dropping explains the
force-unlockfailure.just terraform-aws-inventory force-unlock -force <id>ran bareterraform force-unlockwith no LOCK_ID.Papercut worth its own issue
terraform_runsilently discarding trailing arguments produced two confusing failures in a row: a command that hung for two minutes rather than refusing, and a command that errored on a missing argument the caller did supply. Making it reject unknown trailing arguments rather than swallow them would have surfaced both immediately.Note that simply forwarding all arguments would be the wrong fix, since it would let
apply -auto-approvethrough and quietly defeat the no-auto-approve guard above. The fix is to refuse, not to forward.Corrected status
Everything in the previous comment about what landed still holds and was verified against AWS: nothing was created, and
dig MX coilysiren.mestill returns0 .. Only the diagnosis of why the apply stopped was wrong.Applied 2026-08-27. Verified against AWS.
Kai ran the apply interactively. Plan matched what was reviewed: 9 to add, 1 to change, 0 to destroy.
The domain identity reached
Successimmediately, since the_amazonsestoken and the SES identity were created in the same apply.The alarming-looking output that was not
The apply diff showed
- "grafana.coilysiren.me A",- "eco-mcp.coilysiren.me A", and- "eco-jobs-tracker.coilysiren.me A"leavingroute53_records, androute53_recordsdropping 17 to 16 while a record was being added. Checked before approving: none of those three resolve in DNS at all. They were stale entries in the stored output value, not live records, and0 to destroyconfirmed no resource deletion. Same for the SSM list going 130 to 227, which is a data source refreshing an output, names only and no values.Still unverified: the reject claim
The load-bearing claim, that SES rejects a recipient matching no rule rather than accepting and discarding it, is not yet confirmed. It cannot be probed from Kai's MacBook: outbound 25, 587, and 443 to
inbound-smtp.us-east-1.amazonaws.comall time out, so the network blocks the path entirely.Confirming it needs mail sent from outside the network, which is a Kai action. Two messages from an external account:
coilyco-ops@coilysiren.me, expected to land underinbound/in the bucketcoilysiren.me, expected to bounce back to the senderIf the junk address does not bounce, the bounding argument for reopening the MX fails and the correct response is restoring
records = ["0 ."], which now propagates in five minutes on the reduced TTL.Merging regardless, because the change is already live. Leaving it off
mainwould mean the checked-in configuration no longer describes the running infrastructure, and the next person to apply frommainwould silently revert the MX. A failed bounce test earns a revert commit, not an unmerged branch.The reject claim is now verified. The bound holds.
Tested 2026-08-27 with real mail from an external sender, since outbound port 25 is blocked from the workstation and from the agent sandbox alike (it fails to Google's MX too, so it is generic anti-relay policy rather than anything SES-specific).
Allowlisted recipient,
coilyco-ops@coilysiren.me- accepted and stored. One object underinbound/, 6090 bytes, headers confirming the recipient:Non-allowlisted recipient,
asdfqwer@coilysiren.me- bounced back to the sender, and no object was written to the bucket.That pairing is what settles it. A bounce plus no stored object means SES refused the message at the protocol level rather than accepting and discarding it. Had it silently accepted, there would have been no bounce, and the bucket would have been the only place to notice. Checking the bucket alone could never have distinguished the two, which is why the sender-side bounce was the necessary observable.
So the domain accepts exactly one address and rejects everything else, which is the claim the MX reopening rested on. It is no longer an assumption.
Final posture
coilyco-ops@coilysiren.me. Everything else at the domain bounces.-all, DMARC staysp=reject; sp=reject, both untouched.records = ["0 ."]on the MX, five minutes to propagate on the 300s TTL.Nothing outstanding on this PR. Remaining work is on coilysiren/inbox#440 (verify the address on Tangled) and coilysiren/inbox#466 (converge the agent onto one commit identity, which is what makes this pay for itself).