Conversation
…aws) Sign sts:GetCallerIdentity with the workload's role credentials (env, container endpoint, IMDSv2, ~/.aws/credentials; stdlib only) and post the signed request to /api/devices/enroll/aws. The control-plane host and org id are folded into the signature so a captured request cannot be replayed elsewhere. Credentials never leave the workload. enroll() refactored onto a shared _enroll_request(). Docs: sso-and-provisioning.md, connecting-to-the-platform.md, cli-reference.md.
9581f1c to
595bf38
Compare
Tested on st3ve (real AWS STS round trip)Fresh disposable clone of this PR on the Lightsail box, run against the control-plane counterpart (prismor-web PR #197) on a local dev server backed by the Neon staging DB. Live Suites — full Screenshots live on the throwaway branch |


Problem
A deployed agent on AWS (EC2, ECS/EKS task, Lambda) has no developer machine and no safe place for a shared credential, yet today the only ways onto the control plane are
prismor enroll <token>(a one-time token someone has to hand to the workload) or a long-livedPRISMOR_AGENT_KEYbaked into the deployment. Both are secrets that must be distributed, rotated and revoked by hand. The workload already has a verifiable identity, its IAM role, and Prismor cannot use it.docs/connecting-to-the-platform.mdalready flagged "auth on the enroll UX" as a known follow-up for this path.Prior art — what already exists
identity.enroll()→POST /api/devices/enroll→save_identity(). This PR keeps that flow and swaps what is presented: instead of a token, a SigV4-signedsts:GetCallerIdentityrequest.enroll()was refactored onto a shared_enroll_request(path, payload, ...)so both paths share the POST, the response validation and the identity file. The CLI reuses the existingenrollsubcommand (--aws), soenroll-status,logout, the post-enroll policy pull and the discover seed run unchanged.PRISMOR_AGENT_KEY(identity._env_identity) is the deviceless path, but it is itself a long-lived secret the operator must distribute; it cannot derive identity from the platform the workload runs on. There is no HTTP/JWT/OIDC client anywhere in the runtime (only IMDS blocking inegress.py/policies.py), so nothing existing could sign a request.Tangential updates:
docs/cli-reference.mdrow forenroll;docs/connecting-to-the-platform.mdgains an "AWS workloads" subsection; new customer-facingdocs/sso-and-provisioning.mddocuments the console side (OIDC self-serve, SCIM Users/Groups role mapping, LDAP/AD via the IdP) that ships in the control-plane counterpart PR.Solution — high level
prismor enroll --aws --org <orgId>signssts:GetCallerIdentitywith whatever role credentials the host has (env → ECS/EKS container endpoint → IMDSv2 →~/.aws/credentials) and posts only the signed request toPOST /api/devices/enroll/aws.X-Prismor-Server-Id(control-plane host) andX-Prismor-Org, so a captured request cannot be replayed against another server or into another org; STS also enforces the 5-minute date window.hmac,hashlib,urllib,configparser): no boto, nocryptography, consistent with the rest ofenterprise/.Deep dive — how it works
aws_identity.resolve_credentials()walks the sources the AWS SDKs use, minus config-file role chains. Each source returns{access_key, secret_key, token}orNone; the first hit wins. IMDSv2 is token-required (PUT/latest/api/tokenfirst) with a 1 s timeout so a non-AWS host fails fast.sign_get_caller_identity(creds, region, server_id, org_id)builds a canonical request overcontent-type;host;x-amz-date;[x-amz-security-token;]x-prismor-org;x-prismor-server-idand returns{method, url, headers, body}. The signing key is derived with the standard four-step HMAC and checked in the tests against the AWS documentation vector. Region selectssts.<region>.amazonaws.com(credential scope uses that region; the global endpoint scopes tous-east-1).identity.enroll_aws(org_id, base, label, region)derivesserver_idfrom the--api-basehost, builds the signed request and calls_enroll_request("/api/devices/enroll/aws", {"aws": signed}, ...), which persistsidentity.jsonwithsource: "aws".RuntimeError("no AWS credentials found (checked env, container endpoint, IMDSv2, ~/.aws/credentials)"), exit 1;--awswithout--org/$PRISMOR_ORG_ID→ usage error before any network call; control-plane 401/403 surface with the server'serrorandmessage(for examplecaller is not an IAM role,role_not_bound).~/.aws/config(env/container/IMDS/profile cover the deploy targets), and IMDS hop-limit-1 containers (documented: prefer task/pod roles).Files changed
prismor/runtime/enterprise/aws_identity.pyprismor/runtime/enterprise/identity.py_enroll_request()fromenroll(); addenroll_aws()prismor/runtime/cli.pyenrollgains--aws,--org,--aws-region; error text points at the AWS pathtests/test_aws_identity.pydocs/sso-and-provisioning.mddocs/connecting-to-the-platform.mddocs/cli-reference.mdenrollrow lists the new flagsTesting
Live on st3ve against the control-plane counterpart (real STS round trip, screenshots in the comment below): no credentials → clear error; IAM user credentials →
401 caller is not an IAM role; assumed bound role → service identity minted,enroll-statusverified by the control plane; same role against an org that did not bind it →403 role_not_bound.bash scripts/run_security_tests.shpassesbash scripts/verify_registry.sh(no registry change; still green)Security checklist
Diff size
Lines changed: +557 / −33 · Justification: one new 200-line module is the SigV4 signer + credential resolution, both stdlib; a third of the diff is documentation and tests.