AWS Quick Create onboarding (Launch in AWS) + CloudTrail coverage label fix - #60
Open
akashmaurya3160 wants to merge 18 commits into
Open
akashmaurya3160 wants to merge 18 commits into
akashmaurya3160 wants to merge 18 commits into
Conversation
The design for replacing the manual AWS onboarding flow (download YAML, upload, type two parameters, copy RoleArn, paste it back) with a CloudFormation Quick Create link and a Custom::AuthSecRegistration callback over regional SNS topics into one central SQS queue. Committed before any code so the build can be reverted step by step. Backend only in this branch; the wizard changes come later. The AWS spike (Part C) still has to finish before any of it merges.
The adapter half of automatic AWS onboarding. Nothing calls it yet. Template (version 2026-09-24, role permissions unchanged): - ExternalId is no longer NoEcho. Quick Create ignores NoEcho params, so the Launch link could never have pre-filled it; the value is already readable in the role's trust policy and is not a secret. - Optional CallbackTopicArn and a conditional Custom::AuthSecRegistration that reports RoleArn/ExternalId/AccountId to AuthSec's regional SNS topic, ServiceTimeout 600. Empty topic = the manual flow, unchanged. awsdiscovery: - QuickCreateURL in AWS's documented #/stacks/create/review format; refuses non-commercial partitions and any NoEcho or undeclared param. - CFN callback protocol: SNS envelope, custom-resource request, StackId and topic ARN parsing, response encoding under the 4096-byte limit, PUT with no Content-Type and no redirects. - ValidateResponseURL: exact host match against fixture-backed forms (region without dashes in the bucket name), https/443, no userinfo, presigned query required. Errors never carry the signed query. - CallbackConfig: region->topic map (topic must be in its region, commercial, not opt-in), queue URL, versioned template URL, and the opt-in regions AuthSec's own account can scan.
AWSQuickCreateService wraps AWSOnboardingService (unchanged) and connects accounts only through the existing Onboard(). Sessions (Redis, 1h, single use): validated AWS Region(s) -- commercial only, opt-in regions only if AuthSec's own account enabled them -- a deployment region with a callback topic (moved first, since Onboard probes regions[0]), a fresh ExternalId, and stack/role names sharing an 8-char suffix. Refuses to hand out a link whose template is not published (cached HEAD check). Callback, every check before any AWS call: our topic; well-formed request for AuthSecRegistration; ResponseURL on the allow-list (else never contacted, left for the DLQ); session by sha256(ExternalId) and its workspace binding; topic region == stack region == session region; stack account == role account. Then Onboard() -- AssumeRole with the session's ExternalId and GetCallerIdentity -- and only then SUCCESS. Failure handling: IAM-propagation AccessDenied retried for 90s; AuthSec-side errors retried until SNS timestamp + 540s, then an honest FAILED before CloudFormation's own 600s timeout. Per-session lock, per-request stored result replayed on redelivery, attempt cap. Delete and Update always SUCCESS and never change anything. A 4xx from the presigned URL flags the session. Regional probes run after the answer through the same path a scan uses. Logs carry no signed query and no raw ExternalId.
Drains the central queue every regional callback topic delivers to and hands each message to HandleCallbackMessage. Done deletes the message; Retry makes it visible again in 30s; Reject releases it immediately and never answers it, so after maxReceiveCount it lands in the DLQ and its alarm. Messages in a batch are handled concurrently so one slow Onboard cannot push the others past their callback deadline. Receive visibility is 15 minutes, longer than the 540s callback deadline plus the regional probes; the plan's 5 minutes would have let a slow message reappear to a second replica mid-flight. Started from main only when automatic onboarding is configured and Vault is reachable; AUTHSEC_DISABLE_AWS_CFN_CALLBACK_WORKER=true turns it off. Adds github.com/aws/aws-sdk-go-v2/service/sqs v1.52.0 (no core SDK bump).
POST /authsec/discovery/aws/onboarding/sessions (discovery:admin) starts a
session and returns its Quick Create link; GET .../sessions/:id
(discovery:read) returns its status and per-region results. A session of
another workspace reads as 404. Errors carry their aws_onb_* code:
invalid regions are 422, deployment-side problems 503.
GET /aws/onboarding gains automatic: {enabled, supported_deployment_regions,
default_deployment_region, optin_scan_regions}; enabled:false means the
console keeps the manual flow. Its note no longer calls the ExternalId a
secret.
Flow doc: ExternalId is no longer NoEcho, the two session endpoints, and a Quick Create section with configuration, per-environment infrastructure and operating rules (topics and worker are permanent; alerts; support ref; fixture-backed ResponseURL hosts). Spec: the deliberate deviations from the plan.
The first live run (lab account, stack in ap-south-1) was rejected at the ResponseURL check: CloudFormation sends cloudformation-custom-resource-response-apsouth1.s3.ap-south-1.amazonaws.com, the .s3.<region>. form, while the only form on the list was the legacy .s3-<region>. one from AWS's documentation example. Adds the captured fixture (host and path verbatim, query values redacted) and the form it proves. The legacy form stays, documented as documented-not-observed. Also adds the opt-in live harness (build tag awslive) that drove the run: real template, CloudFormation, SNS, SQS, worker and AssumeRole; Redis and Onboard's Postgres/Vault write substituted.
The second live run panicked on a nil existing-connector lookup (the harness built the service without an onboarding service). Two real problems behind it: a panic in one callback would take the whole backend down, and the message it was holding stays hidden for the 15-minute visibility timeout -- longer than the stack's 600s ServiceTimeout, so the customer's stack fails. The worker now recovers per message and hands it back for a prompt retry. The optional seams are nil-checked; a service with no Onboard answers FAILED instead of dereferencing nil.
Live run in us-east-1 was rejected at the ResponseURL check: CloudFormation sends cloudformation-custom-resource-response-useast1.s3.amazonaws.com there -- S3's legacy home region, global endpoint, no region in the host. Adds the captured fixture and the form, restricted to us-east-1: the same global form for any other region is still rejected.
Fixes from the production-readiness review. Behaviour on the happy path is unchanged; every change is about slowness, failure or duplicates. Worker - B1 No batch barrier: each message starts as it arrives, bounded by 16 in flight per replica. One slow customer no longer holds up everyone else's callbacks until their stacks time out. - B7 Visibility 2 min, extended every minute while a message is handled. A crashed or redeployed worker releases its messages within minutes, inside the stack's 600s ServiceTimeout, instead of stranding them 15. - B2 Reads the queue's maxReceiveCount and passes a last-chance flag: on the delivery before the DLQ a transient failure is answered FAILED, so the stack fails with a reason instead of timing out in silence. Service - B3 No Onboard attempt may still run once a 40s answer reserve before the deadline begins; each attempt's context ends there too. The answer can no longer land after CloudFormation's own timeout. - B4 A delivery that finds the session locked never answers, even past the deadline: its FAILED could overtake the holder's SUCCESS. - B5 After a successful Onboard, a failed session save still answers SUCCESS: FAILED would roll back and delete the role just connected. - B6 InvalidClientTokenId / ExpiredToken / SignatureDoesNotMatch mean AuthSec's own credentials, not the customer's trust policy. - B15 AuthSec-side failures that do not heal (Vault, DB, credentials) fail after 60s instead of making the customer wait the full 9 minutes. Throttling and timeouts still retry to the last safe moment. - B8 Session lock has an owner token, compare-and-delete release, and a keep-alive; TTL 3 min. - B9 Quick Create connections are audited like the manual flow's. - B10 An opt-in region is never the one Onboard probes when a default region is selected. - B11 A panic in a region probe is recovered. - B13 Past the attempt cap nothing is written, and a finished session's lifetime is fixed once: leaked-ExternalId messages cannot grow a session or keep it alive. Each fix has a test.
…ator B12 The Quick Create service is built once per controller, so the template check's cache works; before, every launch paid an S3 HEAD and every GET /aws/onboarding re-parsed the environment. B14 GET /aws/onboarding/sessions/:id is discovery:read, but its launch link and ExternalId let whoever opens them connect an account -- an admin action. Only the admin who started the session gets them back; anyone else sees status and result. Runbook: maxReceiveCount 25 and the worker's own visibility/heartbeat. Spec: records the review fixes, superseding the 15-minute note.
- The launch link and ExternalId from GET /sessions/:id go only to the user who started the session, compared on the per-user id recorded at start. The actor used before can be a workspace-level client id that every user shares, which made the check match everyone. The audit record uses the same per-user id. - The callback worker is built inside its goroutine and reads the queue settings with a 10s bound, so a slow or unreachable SQS can no longer delay backend boot. - The heartbeat is stopped before a message is settled, so a late tick cannot undo the 30s retry / immediate reject visibility. - Past the attempt cap nothing is written, including the dead-URL flag. - Worker concurrency is configurable (AUTHSEC_AWS_CFN_CALLBACK_CONCURRENCY, default 16, max 128) for large onboarding bursts. - The misconfiguration ALERT is logged once, not on every GET /aws/onboarding. - go.mod: service/sqs moved to the direct requirements (it is imported). - Test harness clock guarded by a mutex (the worker test runs handlers concurrently).
Scan coverage showed "CloudTrail events · us-east-1 · ≥ 10000 · Denied" on a
busy account. Nothing was denied:
- RecentEvents reused IAM's maxPages (200), sized for 1,000-item pages and a
runaway marker. LookupEvents returns 50 per page, so it stops at the first
10,000 events of the 48h window -- a cost bound a busy account reaches in
normal operation. It now has its own named cap (unchanged at 10,000) and
says so in the message.
- surfaceResult mapped every non-throttle error to denied, including
ErrTooManyPages. A page cap is now partial ("Partly read" in the console):
the rows are real, the set is not known to be whole. Applies to every
reader using the cap (Bedrock, EKS, Lambda too, though their larger pages
make it unlikely).
Reconciliation is unchanged: only reached is authoritative, so partial
blocks deletion exactly as denied did. CloudTrail events never gate
reconciliation anyway (bonus evidence).
…create-onboarding
Four conflicts, resolved by keeping every line of the graph work and re-applying Quick Create on top: - internal/awsdiscovery/onboarding.go: kept T2.4's comment and the new TemplateOutdated(); TemplateVersion moves from 2026-09-23 to 2026-09-24 so Quick Create's template change stays the newest version. - authsec-aws-discovery-role.yaml: kept T2.4's GetGateway and DescribeRegions grants; version 2026-09-24 in metadata, output and the AuthSecRegistration property. - services/cloud_aws_iam_scan.go: kept the new collectionCoverage() / out-based surfaceResult; the page-cap case is added in the same style. - controllers/platform/cloud_aws_controller.go: kept svc and cursors; the Quick Create fields are added after them. routes.go, cmd/main.go, iam.go and the runbook merged cleanly.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AWS Quick Create onboarding ("Launch in AWS") + CloudTrail coverage label fix
Branch:
aws-quick-create-onboarding→authsec-stagingSummary
Replaces the manual AWS onboarding flow (download YAML → upload → type two
parameters → copy
RoleArn→ paste back) with a CloudFormation Quick Createlink and an automatic callback. The customer picks AWS Region(s), clicks
Launch in AWS, ticks the IAM acknowledgement and clicks Create stack.
Nothing is pasted back.
shows the manual flow exactly as today and the worker does not start.
Onboard()is unchanged and still the only thing that connects an account(AssumeRole with the ExternalId, GetCallerIdentity, account check, Vault, upsert).
(
6545dcc, below) that fixes a coverage label.Design and review history:
.claude/specs/SPEC-aws-quick-create-onboarding.md.Runbook:
docs/flows/aws-cloud-discovery-onboarding.md→ Quick Create.How it works
The callback is never trusted by itself; the topic accepts publishes from any
account. A connection is made only after the checks below and a real AssumeRole
with the session's ExternalId.
Files changed (26: 21 for Quick Create, 5 for the coverage fix below)
internal/awsdiscovery/authsec-aws-discovery-role.yamlCallbackTopicArn+ conditionalCustom::AuthSecRegistration(ServiceTimeout 600).ExternalIdis no longer NoEcho: Quick Create ignores NoEcho params, and the value is already readable in the trust policy. Role permissions unchanged. Version2026-09-24.internal/awsdiscovery/onboarding.goTemplateVersionbump.internal/awsdiscovery/quickcreate.gointernal/awsdiscovery/cfn_callback.gointernal/awsdiscovery/callback_config.gointernal/awsdiscovery/quickcreate_test.go,testdata/cfn_response_urls/*.jsonservices/cloud_aws_quickcreate.goservices/cloud_aws_cfn_callback_worker.goservices/cloud_aws_quickcreate_test.go,…worker_test.goservices/cloud_aws_quickcreate_live_test.goawslive; never runs in CI).controllers/platform/cloud_aws_controller.goautomaticblock inGET /aws/onboarding;POST/GET /aws/onboarding/sessions. The launch link is returned only to the user who started the session.routes/routes.godiscovery:admin, GET needsdiscovery:read.cmd/main.gogo.mod/go.sumaws-sdk-go-v2/service/sqsonly. No other dependency changed.docs/flows/aws-cloud-discovery-onboarding.md.claude/specs/SPEC-aws-quick-create-onboarding.mdCallback trust sequence
Every step runs before any AWS call:
AuthSecRegistration.sha256(ExternalId), and the ExternalId is bound to the session's workspace.Onboard(), then SUCCESS. Delete and Update always answer SUCCESS and never change anything.Failure handling
Onboard(), a failed save still answers SUCCESS.onboard cloud_connector CALLBACK aws-quick-create/<region>/<stack>).Separate fix in this PR:
6545dcc— page cap ispartial, notdeniedScan coverage showed "CloudTrail events · us-east-1 · ≥ 10000 · Denied" on a
busy account. Nothing was denied:
events of 48h.
surfaceResultmapped every non-throttle error, including the page cap, todenied.Files:
internal/awsdiscovery/{iam.go, cloudtrail_events.go, cloudtrail_events_test.go},services/{cloud_aws_iam_scan.go, cloud_aws_surface_result_test.go}.A page cap is now
partial("Partly read" in the console) with a clearmessage. Reconciliation is unchanged: only
reachedis authoritative, sopartialblocks deletion exactly asdenieddid. This touches scan code, soplease review it on its own.
Production prerequisites (before enabling; not needed to merge)
REDIS_URLon the backendsns:Publish. The cross-account policy still needs a second-account test.maxReceiveCount25.TemplateVersion.AUTHSEC_AWS_CFN_CALLBACK_TOPICS,AUTHSEC_AWS_CFN_CALLBACK_QUEUE_URL,AUTHSEC_AWS_TEMPLATE_BASE_URL,AUTHSEC_AWS_OPTIN_SCAN_REGIONS; optionalAUTHSEC_AWS_CFN_CALLBACK_CONCURRENCY(default 16, max 128).sqs:ReceiveMessage,DeleteMessage,ChangeMessageVisibility,GetQueueAttributeson the queue.[aws-onb] ALERTlog lines.Scale
About 1000 concurrent onboardings:
replicas clear a simultaneous 1000-customer burst in minutes, inside the 540s
deadline.
STS throttling.
They already run after SUCCESS. Build it when the oldest-message alarm fires.
Testing
go build ./...,go vetclean. Quick Create + worker: 21 tests pass.Full suite: only
controllers/admin,controllers/sharedandinternal/migrationfail, identically on the base commit, because they needa local Postgres on :5432.
origin/authsec-staging: no conflicts; the merged tree builds and passes.Not in this PR
Organizations/StackSets; region editing (Phase 2 T2.1); GovCloud/China; metrics
and the daily canary; async per-Region probes (deferred, see the spec).