AWS authentication
Chatticus does not create IAM users or mint long-lived IAM access keys.
This is a hard control, not a preference. The organization is transitioning to
AWS IAM Identity Center; every new path must use short-lived credentials from
OIDC, sts:AssumeRole, or an Identity Center–compatible session.
Temporary access keys copied out of an AssumeRole or OIDC session response
are allowed only as in-process forwarding (for example, SAM docker bundling
or a one-shot shell script). Storing those values in GitHub Secrets, SSM, or
Secrets Manager as a standing credential is forbidden. Creating new
long-lived keys — including "temporary" keys left in place as a workaround —
is forbidden.
Allowed patterns
| Surface | Auth path | Where defined |
|---|---|---|
| GitHub deploy workflows | GitHub OIDC → configure-aws-credentials → role-to-assume |
.github/workflows/deploy-*.yml, infra/lib/github-deploy-stack.ts |
| Local / desk deploy | aws login (Identity Center or equivalent short-lived session) |
infra/deploy-*.sh |
| Web CDK bundle (docker) | Forwards the runner's OIDC session into SAM docker | infra/lib/web-build-env.ts (webDockerBundlingEnvironment) |
| Customer cross-account provisioning | Anthus principal sts:AssumeRole into ChatticusOrganizationComputerRole |
conversation/src/computer/provisioning.ts, infra/customer-role.yml |
| Customer computer image publish | Operator aws login → sts assume-role with ExternalId |
computer/push-customer-computer-image.sh |
| Anthus computer image publish | CI: the GitHub OIDC deploy role pushes the image; computer/push-computer-image.sh is break-glass only (operator aws login) |
.github/workflows/build-computer-image-development.yml, computer/push-computer-image.sh |
| Integration test session exchange | SigV4 caller proves an allowed IAM role; control plane returns a bearer token | features/integration_test_auth.feature, infra/lib/integration-test-stack.ts |
| Operator HTTP routes | Bearer secret in Secrets Manager (not an AWS access key) | conversation/src/auth/operator.ts |
| Worker routes | Per-worker minted bearer credential (not an AWS access key) | features/worker_credentials.feature |
Forbidden patterns
Do not add any of the following to this repository or to templates it publishes for Chatticus or customer accounts:
iam:CreateAccessKey,CreateAccessKey,create_access_key,create-access-key- CDK
iam.AccessKey,CfnAccessKey, CloudFormationAWS::IAM::AccessKey - Terraform
aws_iam_access_key aws iam create-access-key(or create-user for standing operator access)- GitHub Actions secrets
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYfor deploy - Scripts that mint keys and write them to disk, SSM, or Secrets Manager
Verify no minting remains
From the repository root:
rg -n 'CreateAccessKey|create_access_key|create-access-key|iam\.AccessKey|CfnAccessKey|aws_iam_access_key|AWS::IAM::AccessKey|iam:CreateAccessKey' \
--glob '!**/node_modules/**' --glob '!**/.git/**' \
--glob '!docs/AWS_AUTH.md' --glob '!infra/test/**'
rg -n 'secrets\.AWS_ACCESS_KEY_ID|secrets\.AWS_SECRET_ACCESS_KEY' .github/workflows/
Production paths must return no matches (exclude this policy doc and guard
tests). CI enforces the same rules in
infra/test/no-iam-access-keys.test.ts and infra/test/deploy-workflows.test.ts.
Legitimate AccessKeyId / AWS_ACCESS_KEY_ID references are limited to:
- STS
AssumeRoleresponse handling (conversation/src/computer/provisioning.ts,push-customer-computer-image.sh) - Forwarding an existing OIDC session into docker (
web-build-env.ts) - Test fixtures and mocks (
conversation/features-support/,conversation/test/)
Rotating the invoke key
The invoke key is one shared value: CloudFront presents it as the
X-Chatticus-Invoke-Key origin header and the Front Door refuses every route
but /health without it (the owner host token and the model gateway excepted).
It is a Secrets Manager secret, not an AWS access key, and it is rotated by
infra/rotate-invoke-key.sh.
Who holds the key and how they get it
| Consumer | How it obtains the key | Staleness after a rotation |
|---|---|---|
| CloudFront (Web stack) | Origin custom header, a CloudFormation dynamic reference resolved at deploy time (web-stack.ts). The Web deploy scripts pin the secret's current version id (-c invokeKeyVersionId=) so a rotation changes the template; without that CloudFormation does not re-resolve an unchanged resource. |
Until the Web stack is redeployed. |
| FrontDoor Lambda | Reads the secret by ARN at cold start and then at most every 60 seconds (INVOKE_KEY_CACHE_SECONDS, conversation/src/auth/invoke-key-source.ts). It accepts the AWSCURRENT and the AWSPREVIOUS value. Before this change it read the secret once per cold start and never again, so a warm container (hours) kept the old key. |
60 seconds. |
| ComputerStarter Lambda | Reads it when it composes its dependencies, now at most every 60 seconds; it is used only to start a customer-account host worker (the key goes into that worker's task environment). | 60 seconds, but see customer workers. |
| Acceptance runner and demo CLI | conversation/bin/acceptance.ts reads the secret by ARN from SSM at every run. chat.ts holds no key. |
None. |
| Browser and static web export | Never. The browser calls /api/* on CloudFront, which adds the header. |
None. |
| Owner containers | Never; they use a signed host token and a gateway token. | None. |
| Customer-account host workers | CHATTICUS_INVOKE_KEY in the task environment the ComputerStarter sets when it starts the worker (host-starter.ts), sent on every host-protocol call and at worker registration. |
Until the worker is restarted. |
| Integration-test sessions | The HMAC of the integration bearer is derived from the key when a Front Door instance composes. | Bearers minted by an instance on the old key fail on an instance on the new key: only integration test and acceptance sessions, only in the minutes around a rotation. |
Development has no customer-account host workers. In staging and production every running customer host worker is refused after the previous value is retired and must be restarted (end the computer and let the next turn start it, or re-provision); rotate during a quiet period and expect an interrupted computer turn for those organizations.
Runbook
Precondition: the Front Door and ComputerStarter that read AWSPREVIOUS are
deployed (the Deploy Web (development) workflow deploys the control plane
first), and aws login / aws sso login for the environment is current.
cd infra
sh rotate-invoke-key.sh development --dry-run # read-only: steps and checks
sh rotate-invoke-key.sh development --yes # rotate
What it does, in this order:
- Refuses without an explicit environment and
--yes; uses the profilechatticus-<environment>(CHATTICUS_AWS_PROFILEoverrides); refuses a secret that is not in the session's account. - Read-only checks: CloudFront and Function URL
/healthanswer 200, the Function URL refuses a keyless request with the invoke key gate, CloudFront passes the gate. get-random-password(40 characters, no punctuation) held in memory, thenput-secret-valuethrough stdin. The old value becomesAWSPREVIOUS. The Front Door accepts both values from here on.- Waits 65 seconds so every warm Front Door has read the pair.
- Redeploys the Web stack (development:
gh workflow run "Deploy Web (development)" --ref developand watches it; staging and production:deploy-chatticus-dedicated-account.sh <environment> webfrom the operator session), then waits for the distribution to beDeployed. - Compares the distribution's origin header with the
AWSCURRENTvalue in memory (never printed). Only if equal does it go on. - Retires
AWSPREVIOUS(update-secret-version-stage), waits 65 seconds and re-verifies the open and the protected routes.
If any step fails the script exits non-zero with the reason. Before step 7
nothing is down, because both values are still accepted. Fix the cause and run
--resume --yes, which finishes steps 4 to 7 without a new value; a normal run
refuses while a previous value is staged. If gh is missing or not logged in
the script prints the exact gh workflow run command and exits 3; run it, wait,
then --resume --yes. Afterwards run the acceptance runner
(cd conversation && npx tsx bin/acceptance.ts --environment development).
The window
There is no refusal window for web users: for the whole time CloudFront still
carries the old key the Front Door accepts it as AWSPREVIOUS, and the first
wait keeps the deploy from beating the Lambda cache. The only exposure is the
old key staying valid until step 7; if it is suspected leaked, do not stop
between steps 3 and 7. If the Web deploy is done by hand with a CloudFront that
takes long to propagate, nothing is lost: both values work. After retirement
the old key is dead everywhere within 60 seconds.
Cadence
Rotate every 90 days, after anyone with cloudfront:GetDistributionConfig,
secretsmanager:GetSecretValue on the secret or ecs:DescribeTasks on the
host cluster leaves the operator set, and at once on any suspected exposure.
Proposed, not built: take the key out of the CloudFront configuration
Today the key is plain text in the distribution configuration. The fix is an
origin access control for the Function URL: set the Function URL authType to
AWS_IAM and let CloudFront sign the origin request (SigV4) with an OAC, so no
shared header exists. Known obstacles from this repo: the STS authorization
relay (integration-test and caller-identity proof, buildStsGetCallerIdentityHeaders)
and the worker bearer travel in the Authorization header, which the OAC
SigV4 signature would overwrite; direct Function URL callers (customer-account
host workers, the acceptance runner) would need to sign requests themselves or
use a second, separately protected URL; and the Function URL would need a
resource policy for cloudfront.amazonaws.com scoped to the distribution ARN.
Until then the rotation above is the control.
Ops cleanup after cutover
This repository does not mint keys, but historical operator practice and sibling repos may have left standing credentials. After this control lands, deactivate and delete any long-lived keys that are not tied to an active break-glass exception:
| Implied principal | Evidence in repo | Action |
|---|---|---|
| GitHub Actions deploy (Anthus account) | infra/README.md OIDC section; workflows use AWS_DEPLOY_ROLE_ARN only |
Confirm no AWS_ACCESS_KEY_ID GitHub secrets remain on AnthusAI/Chatticus environments |
| Lab / throwaway customer account operator | project/wiki/runbooks/throwaway-account-provisioning.md records IAM user chatticus-b88c0a-operator |
Deactivate access keys; prefer Identity Center or OrganizationAccountAccessRole session |
| Desk / kernel operator sessions | AGENTS.local.md (gitignored) may reference role ARNs |
Audit aws iam list-access-keys in Anthus and customer lab accounts |
| Sibling repos | Not in this checkout | Audit AnthusAI/Chattic.us-web and any Anthus operator/kernel automation for create-access-key |
Use aws iam list-users and aws iam list-access-keys --user-name <user> (or
Identity Center permission sets) in each account. Do not create replacement
keys while cleaning up — use aws login / Identity Center and existing
AssumeRole paths instead.
Related docs
infra/README.md— GitHub Actions OIDC one-time setupAGENTS.md— agent hard control (summary)infra/customer-role.yml— customer cross-account role (roles only, no users/keys)