feat(cloud): fail launch preflight when a private hosted zone shadows a bootstrap download host - #7553
Conversation
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
506974d to
73c99c7
Compare
Disposition: PR-template warning — fixedThe description bot's warning above is stale. All four named sections were added, using the repo template's exact heading strings (including the unicode arrows in
Also present: Reading the template surfaced two things the bot didn't flag, both now corrected:
Two gates left, both maintainer-side1. The AI review lanes are 2. The four red checks are pre-existing on this PR's base and will not clear from anything done on this branch. See the Base-branch CI status section in the description: The |
… a bootstrap download host An interface VPC endpoint with private DNS enabled creates a private hosted zone authoritative for its whole domain. Amazon Q's com.amazonaws.<region>.q endpoint creates one for q.<region>.amazonaws.com, and kiro-cli is downloaded from desktop-release.q.us-east-1.amazonaws.com -- inside that subtree. The lookup is answered by the private zone, finds no record, and returns NXDOMAIN without falling through to public DNS, so the bootstrap fails minutes later with "kiro-cli did not install", naming the wrong layer. Check it before provisioning: one route53:ListHostedZonesByVPC call at the single point where the --subnet and auto-discovery paths converge. A missing permission is non-fatal so an older launch policy keeps working. Refs kirodotdev#7522
73c99c7 to
63c3044
Compare
|
The one remaining red here is not this PR, and I can now show that rather than assert it — plus the fix is up separately, which changes what I'm asking for.
Both It is not actually a flake, and that matters for what you do about it. What makes it look intermittent in CI is So I'm withdrawing my earlier request to re-run this job. A re-run is a ~78% coin flip that fixes nothing and will re-redden someone else's PR later today. I've put the actual fix up instead: #7852 — one import plus one autouse reset fixture in that test file, matching what What would help here, in preference order:
Also still outstanding and not something I can move: the five AI review lanes are Everything else is green. |
|
Closing: this is redundant. The preflight it adds is on For the record, since a silently closed PR reads as a withdrawal: #7888's 197 added lines are byte-identical to this PR's, across the same four files. Both added-line sets hash to I've written that up as a process question in #8046 rather than leaving it implied here, including what I am explicitly not claiming — I am not alleging misconduct by anyone, and @aniruddhaadak80 has several of their own fork PRs closed unmerged in the same window, so they are in the same position I am rather than an advantaged one. The useful outcome is PR-level de-duplication and a fix for the Stage-2 fork-review gap, not attention on these two PRs. The investigation behind this change is in #7522 (root cause, the controlled A/B) and the durable fix — serving the kiro-cli artifact from a domain no customer-attachable private hosted zone can claim — remains open and unaddressed in #7822. #7888 landed detection, which is the same scope this PR had; it does not remove the namespace collision. Thanks to whoever reviewed and merged #7888 — the check is better in the tree than in an open PR, which is why I'm not asking for this one to be reopened. |
Problem / Motivation
kirocrew cloud launchcan fail deterministically, ~4 minutes in, with an error that names the wrong layer:The real error, visible only in the untruncated reason:
A private hosted zone owns its apex and every subdomain. An interface VPC endpoint with private DNS enabled creates one — Amazon Q's
com.amazonaws.<region>.qendpoint creates a zone forq.<region>.amazonaws.com. kiro-cli is downloaded fromdesktop-release.q.us-east-1.amazonaws.com, which sits inside that subtree, so the lookup is answered by the private zone, finds no record, and returns NXDOMAIN without falling through to public DNS.Nothing in the preflight looks at the resolver path.
cloud doctorand[1/6]–[2/6]validate the aws CLI,session-manager-plugin, and EC2/CloudFormation/SSM reachability — all of which go green — and then the launch spends ~4 minutes creating an IAM role, security group, instance profile and EC2 instance before dying on a condition that was knowable from one API call up front.Why it matters
discover_network()targets the account's default VPC by design (isDefault=true,ec2.py), so a Q endpoint in the default VPC means remote crew cannot launch at all — deterministically, on every attempt. Q endpoints via PrivateLink are common in enterprise and regulated accounts, and such an account is more likely to have one, not less.The cost is not just the wasted provisioning, it is the misdirection. In the reported case this took six launches to identify, and produced two confidently wrong root causes on the way:
fail()reason budget (~1 KB) was filled with successfuldnftransaction output —brotli,nodejs 18,python3.11-setuptools— while the actual error was truncated away. Those RPM names look exactly like a failing package install, so the first three attempts chased a "transient dnf mirror race".--retryon thatcurllooked like the cause. Adding--retry 5 --retry-delay 2 --retry-connrefusedand relaunching produced 6 consecutive failures over ~10 seconds — proving the failure is deterministic and that retries cannot help.Meanwhile
cdn.amazonlinux.comandnodejs.orgresolve fine in the same run, which is what makes this look impossible until you inspect the VPC's resolver config.What changed (motivation → approach → change)
Goal: turn a deterministic, misattributed, post-provisioning failure into a pre-launch error with a working suggestion.
Approach. Detect the general condition rather than special-casing Amazon Q: "is any private hosted zone bound to the selected VPC authoritative for a host the bootstrap must download from?" One read-only call, no new infrastructure, and it catches future shadowing by any endpoint or user-created zone. Rejected alternatives: resolving the name from the launching machine (wrong vantage point — the machine outside the VPC resolves it fine), and probing from the instance (too late, the instance is the thing we are trying not to create).
What was built:
_BOOTSTRAP_DOWNLOAD_HOSTS— the hosts the UserData fetches. The kiro-cli URL is pinned tous-east-1in the template regardless of launch region, so the constant is literal to match rather than interpolating the region._zone_shadows_host()— pure, label-boundary suffix match. A plainendswithwould reportxq.us-east-1.amazonaws.comas shadowingdesktop-release.q.us-east-1.amazonaws.com; this does not.shadowed_download_hosts()— oneroute53:ListHostedZonesByVPCcall, returns(host, zone)pairs.assert_download_hosts_resolvable()— raisesaws.AWSErrornaming the zone, the host, and the--subnetremedy.--subnetand auto-discovery paths converge, so both are covered by one call site, inside the existingtrythat cleans up the uploaded source on failure.iam.py: newRoute53DnsPreflightstatement.ListHostedZonesByVPCdoes not support resource-level permissions, henceResource: "*".A missing permission is deliberately non-fatal.
iam.pyis an explicit action allowlist with no describe wildcard, so anyone running an older launch policy has noroute53:ListHostedZonesByVPC. OnAWSErrorthe check logs and returns empty. Losing the early warning is acceptable; breaking a launch that would otherwise succeed is not.Design question for reviewers
A positive finding is currently fatal. The tradeoff: it can false-positive. If someone runs a private zone for
nodejs.orgpointing at a legitimate internal mirror, resolution works today and this check would start blocking them.Alternatives if you would rather not take that risk:
*.amazonaws.com), warn for the rest — the collision this addresses is in that namespace, a third-party internal mirror never is;ListResourceRecordSetsbefore raising. This needs another permission and does not always work: the Q endpoint's zone is service-managed and returnedAccessDeniedwhen enumerated.Happy to switch to any of these — say which and I will update.
Tests
pytest test/test_cloud_ec2.py test/test_cloud_iam.py→ 139 passed.7 new tests in
TestDnsPreflight(test/test_cloud_ec2.py), mocking AWS at thecloud.aws.checked_jsonchokepoint per the file's existing convention:test_zone_shadows_subdomain_and_apextest_match_is_on_label_boundariesxq.…/notnodejs.orgdo not match — theendswithfalse positivetest_empty_zone_never_shadows.) zone names are inerttest_detects_q_endpoint_zoneq.us-east-1.amazonaws.comflags the kiro-cli host,efs.…does nottest_clean_vpc_has_no_hitstest_missing_permission_is_not_fatalAccessDeniedreturns empty and the assert wrapper stays quiet — the backwards-compat guaranteetest_assert_raises_with_actionable_text--subnet, so the user gets a way forward and not just a diagnosisExtended
test_covers_core_launch_actions(test/test_cloud_iam.py) soroute53:ListHostedZonesByVPCis guarded by a test rather than only present in the policy.Manual verification
Verified against a live account, as a controlled experiment — same instance size, region, AMI and template, with only the VPC changed:
com.amazonaws.us-east-1.q, private DNS on)desktop-release.q.us-east-1.amazonaws.comCREATE_COMPLETE, signed in, dashboard upThe read-only calls this change relies on were exercised by hand against both VPCs:
route53 list-hosted-zones-by-vpcreturnsq.us-east-1.amazonaws.com.for the failing VPC and no shadowing zone for the working one — i.e. the check would have raised on the first and passed on the second, before any resource was created.One limit worth stating: the private zone's record set could not be enumerated (
AccessDenied— it is service-managed by the endpoint), so "the zone has nodesktop-releaserecord" is inferred from the observed NXDOMAIN rather than read directly. The A/B narrows the alternatives considerably but does not independently rule out some other property of that VPC.Why no screenshot: backend-only change — two Python modules under
src/kiro_crew/cloud/and their tests. No frontend path is touched and nothing renders differently.Related Issues
Fixes #7522
Scope of that
Fixes, stated explicitly. This PR resolves what #7522 reports — a launch that dies ~4 minutes into provisioning with a failure reason naming the wrong step — by detecting the shadowing before any resource is created. It does not remove the underlying namespace collision. That durable fix (serve the kiro-cli artifact from a domain no customer-createable private hosted zone can be authoritative for) is tracked separately in #7822, so it survives this merge instead of closing with it.I originally withheld the closing keyword to keep the scope honest. In practice that was the wrong call: with no linked PR,
closedByPullRequestsReferencesstayed empty, automated triage lanes screened #7522 as uncovered and re-selected it repeatedly, and one of those passes built a diagnosis on the retracted root cause still sitting in the issue body. The body now carries a retraction banner, and splitting the durable fix into #7822 keeps the tracker accurate without leaving the reported defect unlinked.Also unaddressed here, and carried into #7822:
fail()'s ~1 KB reason budget keeping successfuldnfoutput while truncating the actual error away, and the failure message naming the install rather than the download.Pattern harvest
Rule candidate:
review-promptPattern: a host the bootstrap must reach lives under a domain that a VPC-endpoint private hosted zone can be authoritative for — any such host is unreachable inside that VPC regardless of public DNS, and no retry helps.
The generalizable form is the check itself (suffix-match every required download host against the VPC's private zones) rather than a lint rule, which is why it ships as a preflight instead of a static check. The narrower reviewable invariant: a new hard-coded download host in the bootstrap should be added to
_BOOTSTRAP_DOWNLOAD_HOSTSin the same change, or the preflight silently stops covering it.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)--subnetremedy inlineCI status after rebase
Rebased onto current
main(af861783). The four checks previously red here were failing identically on the old base5603ae7(test_security_posture.py::…test_no_new_gate_side_log_line_reads_the_baseline_redactor, ondashboard/handlers/memory.py). That has since been fixed onmain, and the rebase cleared them:5603ae7af861783backend-test=failure)The rebase reused this branch's four file blobs on top of
main's current tree, so the diff is byte-identical (+197/−0, same four files) and the branch is nowbehind_by: 0,MERGEABLE.One remaining red is a pre-existing test-isolation bug, not this change
Backend Tests (Windows) (3)now fails on a different, unrelated test — a rate limiter tripping under parallel execution:The census test no longer appears in that job's annotations, none of this PR's files are implicated, and session-creation rate limiting has no path to the
route53:ListHostedZonesByVPCcall this PR adds.retry shortlyis the limiter's own transient message. The same shard passes on3.10and3.12.Root-caused since, and it is not actually a flake — the fix is up as #7852.
create_rate_limit._bucketsis process-wide module state keyed(verb, caller_key). Every test intest/test_session_control.pybuilds its caller as_slot(state, "chat-1"), so all 36create_session(call sites share one bucket key against a budget of 20 per 300 s; the file runs in ~1.5 s, so the creates accumulate and the 21st onward is refused. Running the file whole fails deterministically (2 failed, 140 passed);pytest-splitdistributing it across 4 groups is what makes it look intermittent, and is also why the same shard number passes on 3.10/3.12.mainshows the same two tests failing in 2 of the 9 runs that completed that job in a ~5.5-hour window (33604886863, 33599188871), with byte-identical annotations — full table in this comment.So please don't re-run this job on my behalf — it is a ~78% coin flip that fixes nothing and will re-redden another PR later. Merging #7852 (one import, one autouse reset fixture, matching
test_create_rate_limit.pyandtest_chat_folder_cap.py) clears it here and everywhere. Failing that, this red is safe to accept as pre-existing: this PR's diff has no path todashboard/session_control.py, and aroute53:ListHostedZonesByVPCcall cannot reach a session-creation rate limiter.