Security overview
This document is for security reviewers. It covers what Cloud Certainty StateCraft protects against, how users are identified, how access is limited, where data and audio go and how they are encrypted. Everything described here runs in your own AWS account.
Threat model
StateCraft lets many people see, run and change AWS Step Functions workflows, by mouse, keyboard or voice. What it must prevent:
| Threat | Mitigation |
|---|---|
| A user sees or acts on state machines no one granted them | Two independent layers: grants checked by the API on every call, and IAM credentials that a per-request session policy limits to the granted state machines, their versions, aliases, executions and Map Runs, and the granted actions (see Permissions) |
| A user reads execution data they shouldn't | The data permission is separate from view. Without it, StateCraft asks Step Functions for metadata only and removes inputs, outputs and causes; for state machines encrypted with a customer managed key, the user's AWS session has no decrypt permission either |
| Someone stops, redrives or deploys by mistake or on purpose | run and deploy are separate grants and need a connection whose role allows them (Operate, Full); deploys refuse to overwrite a change made since the person opened the state machine; every start, stop, redrive, Map Run change, TestState run, deploy, version and alias change is audited with the person's email |
| A voice command or an AI proposal changes a workflow silently | Voice and typed commands are a deterministic grammar whose every change shows at once in the graph and the code, with undo; assist proposals are shown as a diff and applied only when the person accepts; neither deploys by itself, and "deploy" asks for confirmation |
| Audio or workflow definitions leave the organisation | Voice uses Amazon Transcribe streaming in your account by default, and the browser's speech service only when an administrator chooses it (the portal says where audio goes); assist uses Amazon Bedrock in your account; StateCraft stores no audio or transcripts |
| A bulk job keeps acting after someone loses access | The bulk workflow rechecks the job owner's grants for every item and acts with the owner's own scoped session |
| StateCraft passes a powerful role to a new state machine | iam:PassRole is allowed only to Step Functions, only for the roles in the stack's PassRoleArns parameter, and never for StateCraft's own roles; people without deploy can test a state only with the state machine's own role |
| Someone impersonates another user | Federated sign-in; the API trusts only emails the user can't choose, and binds the bootstrap admin to an immutable ID (see Identity) |
| A stolen token is used from elsewhere | One-hour ID tokens, revocable 8-hour refresh tokens, optional IP allowlist checked on every call |
| StateCraft is used to change its own stack | The roles users act through have no IAM permissions other than iam:PassRole to Step Functions, and no CloudFormation, DynamoDB or StateCraft bucket permissions |
| Actions can't be traced | Application audit log and execution records with who did what, plus SourceIdentity = <user email> on every assumed-role session, so CloudTrail in every account names the person behind each Step Functions call |
| Another account's role is assumed by a third party ("confused deputy") | Spoke roles trust only the hub's API and bulk worker role ARNs and require an external ID and a source identity |
| An execution ARN from the browser points somewhere else | The API never trusts an ARN from the browser: it parses it and checks its partition, account, Region and state machine against the request and the grants |
| Data leaves your organisation | No vendor service in the data path; the stack never contacts the vendor at runtime |
Out of scope: a malicious administrator of the AWS account (they can change any IAM policy), and the security of the identity provider, of users' devices and browsers, and of the browser vendor's speech service when an administrator chooses it.
Controls
| Principle | Control |
|---|---|
| Strong identity on every request | Every API route requires a Cognito ID token, validated by the API Gateway JWT authorizer (issuer and audience). Admin routes check the role on each call. |
| Least privilege | Each component has its own role with resource-scoped permissions (IAM summary). Users act only through roles under the IAM path /statecraft/, narrowed per request to their grants. |
| Context-aware access | IP allowlist checked on every call (Business and above); outside it, every route except the sign-in check returns 403 ip_not_allowed. |
| Short-lived credentials | STS sessions of 15 minutes per user and connection. Voice sessions use a presigned Amazon Transcribe URL valid for at most 5 minutes, for one person. |
| Strong authentication | Your identity provider's MFA applies. Cognito-only users must use TOTP MFA and passwords of at least 14 characters. User-existence errors are suppressed. |
| Separation of duties | Access levels per account (Read, Operate, Full) and permissions per state machine pattern (view, data, run, deploy). |
| Hardened portal | Strict CSP, HSTS, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a Permissions-Policy that allows only the microphone and clipboard writing, for the portal itself, no source maps (Portal). |
| Auditability | Execution records with the person, application audit log, API access logs, and CloudTrail in every account with the user's SourceIdentity. |
Identity and authorization
The API trusts the Cognito ID token's signature, issuer and audience (checked by API Gateway), and
then decides who the caller is from its claims. Roles, grants, drafts and the bootstrap admin are
keyed on the email claim, so a user who could pick their own email could take over someone
else's access. The API therefore accepts the email only when it is one the user can't choose;
otherwise it refuses the caller with 403 forbidden on every route.
| How the user signs in | Email trusted when | Why |
|---|---|---|
| SAML: IAM Identity Center, Entra ID, Okta | always | The email is asserted by your directory and signed by the IdP. Map it from an attribute users can't edit (Identity providers). |
| OIDC | email_verified is true | Many OIDC IdPs let users or guests set their email. The stack maps the IdP's verification claim to Cognito's email_verified. |
| Cognito user pool user | email_verified is true | Users are created by administrators and can't change their email. |
The provider is read from the signed token, never from anything the client sends.
Bootstrap admin. BootstrapAdminEmail is always an admin, so it is bound to an immutable
identity: the first trusted sign-in with that email records the caller's Cognito user ID, and from
then on only that user is the bootstrap admin. Sign in as the bootstrap admin straight after
deployment. Resetting the binding is described in the admin guide.
Permissions, grants and session policies
What a user can do is the intersection of two independent layers.
- The connection's IAM role sets the ceiling for everyone: the local role is limited by the
stack parameters
LocalAccessLevel(Read,OperateorFull),PassRoleArnsandStateMachineKmsKeyArns; a spoke role by the same parameters of its own stack. StateCraft can never do more in an account than its role allows, and the account owner controls that role. - StateCraft grants decide what each user may do within that ceiling: a subject (user or
group), a connection, Regions, state machine name patterns and permissions (
view,data,run,deploy). See the admin guide.
Grants are not only checked in code. For every request, the API calls sts:AssumeRole on the
connection's role with:
SourceIdentity= the user's email. It can't be changed for the rest of the session, also in role chains, and it appears in CloudTrail for every Step Functions, CloudWatch, CloudWatch Logs and KMS call in the target account;- a role session name
sc-<email>, also visible in CloudTrail; - a session policy (for non-admins) that allows Step Functions actions only on the ARNs of the
state machines matching the user's patterns (the machines, their versions and aliases, their
Standard and Express executions, their Distributed Map children and Map Runs), and only the
actions of the user's permission: reads for
view; reads withkms:Decryptfordata; plus start, stop, redrive, TestState, Map Run updates,iam:PassRoleto Step Functions andkms:GenerateDataKeyforrun; plus create, update, delete, versions, aliases, tags and log delivery fordeploy. If a user's grants are too many to fit the session policy size limit, the request is refused rather than widened.
Listing state machines and reading CloudWatch metrics have no resource-level permissions in AWS, so those reads use a separate, read-only session, and StateCraft filters the results by the caller's grants: a user never sees a state machine they have no grant on.
Data without data. IAM can't separate an execution's metadata from its data. For people without
data, StateCraft asks Step Functions for metadata only (includedData = METADATA_ONLY,
includeExecutionData = false) and removes inputs, outputs, causes and parameters from what it
returns. For state machines encrypted with a customer managed key, AWS enforces it as well.
Your SCPs, permission boundaries and key policies still apply on top: StateCraft adds restrictions and never removes yours.
Voice and assist
| Path | What is sent | What is kept | |
|---|---|---|---|
| Voice with Amazon Transcribe (default) | Browser → Amazon Transcribe streaming in the stack's Region, over a WebSocket URL the API signs for that person (5 minutes, signed by a role that may only start Transcribe streams, assumed with the person's email as SourceIdentity) | Microphone audio while the microphone is on | Nothing in StateCraft; the number of sessions in usage. Amazon Transcribe may use content to improve AWS services unless your organisation has an AI services opt-out policy |
| Voice with the browser's speech service (only if an administrator chooses it) | Browser → the browser's speech recognition; in Chrome and Edge, the browser vendor's web service, outside your AWS account | Microphone audio while the microphone is on | Nothing in StateCraft. The portal tells people where their audio goes |
| Typed and spoken commands | In the browser: a deterministic grammar turns text into changes to the draft or into navigation | Nothing | The draft's autosaved versions |
| Assist (Business, off until an administrator turns it on) | API → Amazon Bedrock in your account (AssistRegion), Claude model AssistModelId | The person's description and, for a change, the current definition | The first 500 characters of the description in the audit log; the proposal only if the person accepts it into a draft |
IAM summary
| Role | Assumed by | Permissions |
|---|---|---|
API role (<prefix>-api-<region>) | Lambda | Its two DynamoDB tables; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /statecraft/ only; start the bulk jobs workflow; bedrock-mantle:CreateInference on the assist model only (when AssistModelId is set); read the account's licences in AWS License Manager; its log group; KMS only through DynamoDB. No Step Functions access to your state machines of its own |
Local role (/statecraft/<prefix>-local-<region>) | API role and bulk worker, with a source identity | By LocalAccessLevel: Step Functions reads, CloudWatch GetMetricData, CloudWatch Logs reads for Express executions (Read); plus start, stop, redrive, TestState and Map Run updates, and iam:PassRole to Step Functions on PassRoleArns, never StateCraft's own roles (Operate); plus create, update, delete, versions, aliases, tags and log delivery (Full). KMS only through Step Functions (kms:ViaService), limited to StateMachineKmsKeyArns when listed |
Spoke role (/statecraft/<RoleName>, other accounts) | API role and bulk worker role (the hub's SpokeTrustedPrincipal), with the external ID and a source identity | As the local role, by its stack's parameters |
Voice role (/statecraft/<prefix>-voice-<region>, with Voice = Enabled) | API role, with a source identity | transcribe:StartStreamTranscriptionWebSocket only |
| Bulk workflow and worker roles | Step Functions and Lambda | The workflow (a Standard state machine whose items run as Express child executions of a Distributed Map) may invoke only the worker; the worker reads and updates bulk job items in the table and assumes connection roles as the job owner |
| Update notifier role | Lambda | Its settings and audit items; publish to the update topic |
| Portal deployer role | Lambda | Deploy the portal's Amplify app from one file in the artifacts bucket |
| Installer roles | ECS (one-off task) and Lambda | Run the installer task; read the account's licences; pull the image from the AWS Marketplace registry; write the code to the artifacts bucket |
* resources appear only where AWS has no resource-level permissions (states:ListStateMachines,
ListActivities, ValidateStateMachineDefinition, TestState, cloudwatch:GetMetricData, log
delivery actions, License Manager), for PassRoleArns when you keep its default * (still only
to Step Functions, and never StateCraft's own roles), and for KMS keys when StateMachineKmsKeyArns
is empty (still only through Step Functions).
Data flows
| Flow | Path | Protection |
|---|---|---|
| Sign-in | Browser ⇄ Cognito managed login ⇄ your IdP | TLS; SAML signatures or OIDC; authorization code flow with PKCE |
| API | Browser → API Gateway → Lambda | TLS 1.2+; JWT on every call; CORS limited to the portal origin; throttled |
| Step Functions | Lambda → STS → Step Functions, CloudWatch and CloudWatch Logs in any connected account | TLS; per-request, per-user scoped STS credentials with SourceIdentity |
| Bulk jobs | API → StateCraft's bulk workflow → worker Lambda → STS → Step Functions | TLS; the job owner's scoped session, grants rechecked per item |
| Voice | Browser → Amazon Transcribe streaming in your account | TLS (WSS on port 8443); a presigned URL for one person, valid 5 minutes |
| Assist | Lambda → Amazon Bedrock (Mantle endpoint bedrock-mantle.<region>.api.aws) in your account | TLS; only the description and the current definition |
| Licence | Installer and API → AWS License Manager in your account | TLS. A read of the account's licences for the product |
| Code install | AWS Marketplace container registry → installer task → artifacts bucket | Image pulled by digest; every file checked against a SHA-256 manifest; written to a private, TLS-only bucket |
What stays in your account
Everything: identities, connections and external IDs, grants, drafts (workflow definitions),
saved inputs (which may contain business data), execution records, bulk jobs, usage figures and the
audit log, as well as your state machines, their executions and their data. Cloud Certainty operates
no service in the data path, and the stack never contacts the vendor at runtime. The AWS Marketplace
container registry and the vendor's public bucket in us-east-1 (templates and the release manifest)
are read-only distribution points used at install and upgrade, and, with UpdateChecks = Enabled,
for a daily read of the public release manifest that sends nothing about you. Links to
cloudcertainty.com in the portal are plain links, followed only when someone clicks them. The one
path out of your AWS account is voice with the browser's speech service, and only when an
administrator chooses it.
Encryption
- In transit. TLS for every AWS API, Amazon Transcribe streaming and the portal (HTTPS only, with HSTS). The artifacts bucket denies non-TLS requests.
- At rest. With
KmsKeyArn, your customer managed key encrypts the DynamoDB tables, CloudWatch Logs, the update topic, the artifacts bucket and the bulk jobs workflow. Without it, AWS owned keys and SSE-S3 are used. Your state machines keep their own encryption; StateCraft uses their keys only through Step Functions, only within a person'sdataorrunpermission, and only the keys inStateMachineKmsKeyArnswhen you list them.
Portal hardening
Amplify Hosting serves the portal with these headers:
- A strict
Content-Security-Policy: no inline or third-party script, noeval,frame-ancestors 'none', and AWS endpoints allowed only for sign-in, the API and, for voice, the Amazon Transcribe streaming endpoint of the stack's Region. Strict-Transport-Security,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, and a Permissions-Policy that allows only the microphone and clipboard writing, for the portal itself, and turns off camera, geolocation, payment and USB. Browsers ask each person before the portal can use the microphone.
Graphs, timelines and charts are drawn in the browser; nothing is loaded from third parties. Tokens are kept in session storage, never in cookies; refresh tokens last 8 hours and can be revoked.
Audit
Every administrative change (roles, connections, grants, settings), every deploy, version and alias
change, every execution started, stopped or redriven, every Map Run change, TestState run, bulk job,
draft change and assist request is written to the audit log with the actor, source IP and target.
Admins read it in Administration › Audit log. Events are kept for the plan's audit days: 30 on
Starter, 90 on Team, 365 on Business and 3,650 on Enterprise. Execution records keep who started,
stopped and redrove each execution for 30 to 730 days by plan. For each AWS call, CloudTrail in the
target account shows the assumed-role session with sourceIdentity set to the user's email. Forward
CloudTrail and the log groups to your SIEM.
Licensing
StateCraft reads its AWS Marketplace licence from AWS License Manager in your account: at install and
in the API. Nothing about your usage or data is sent anywhere. Without a valid licence the installer
stops and the API answers 402 licence_required. If License Manager is unreachable, or a licence
that was active disappears (which bridges a move between plans), StateCraft keeps working for up to
72 hours from the last successful check. Your data and your workflows stay in place either way.
Shared responsibility
- Keep
BootstrapAdminEmailon a protected identity and enforce MFA at your IdP. Grant Admin to a second person or group. - Keep connection roles at
Readunless people need to operate executions there, and atOperateunless teams deploy through StateCraft. NarrowPassRoleArnsto your workflows' execution roles. - Grant groups rather than single users, with name prefixes per team (
orders-*), and givedataonly to people who need to see execution data. - Keep voice on Amazon Transcribe, set an AI services opt-out policy if your organisation requires it, and choose the browser's speech service only if sending audio to the browser vendor is acceptable.
- Review assist proposals like any code change before deploying them.
- On Business and above, set the IP allowlist to your corporate egress ranges if users should only connect from managed networks.
- Forward CloudTrail and StateCraft's log groups to your SIEM.
Something unclear or missing? Email [email protected].