Prerequisites
An AWS account with console access. No special setup, no CDK bootstrap, no pre-existing infrastructure.
STELR's template uses BootstraplessSynthesizer —
the only thing the deploying IAM principal needs is permission to create the resources the stack touches
(KMS, Lambda, DynamoDB, S3, Step Functions, API Gateway, EventBridge, SQS, SNS, IAM roles).
If your account enforces permission boundaries, see the required IAM policy.
Subscribe on AWS Marketplace
Go to the STELR listing and click Continue to Subscribe. Accept the terms. Marketplace triggers automatic provisioning on STELR's side — no separate form or signup.
STELR on AWS Marketplace ↗- Customer ID — a UUID you enter when deploying the stack (e.g.
a3f7c821-09be-4d12-b501-8e6f3c112a04) - Registry JWT (Attestation Token) — used in Step 3 to pre-create an SSM parameter
- API Key — the key you'll use in every API call; also goes into SSM in Step 3
If the email doesn't arrive within 5 minutes, check spam or contact support@stelrai.com.
Pre-flight: create two SSM parameters
Do this before deploying the stack
The stack deploys a Lambda (stelr-key-registrar-customer) that you invoke once after deployment.
It reads these two SSM parameters to register your signing key with STELR's registry and seed your API credentials into your stack's DynamoDB table.
Create them in the same region you'll deploy to — both values are in the onboarding email from Step 2.
# Registry JWT — authenticates the key registration with STELR aws ssm put-parameter \ --name /stelr/registry/attestation-token \ --value "<Registry JWT from your STELR onboarding email>" \ --type SecureString \ --region us-east-1 \ --profile <YOUR-AWS-PROFILE> # API key — used in the X-API-Key header on every call aws ssm put-parameter \ --name /stelr/api-key \ --value "<API key from your STELR onboarding email>" \ --type SecureString \ --region us-east-1 \ --profile <YOUR-AWS-PROFILE>
Launch the CloudFormation stack
From the STELR listing on AWS Marketplace, click Launch. The deployment form shows one field under STELR Configuration: STELR Customer ID (from onboarding email). Paste the UUID from your email. That is the only value you supply.
Click Launch CloudFormation. The stack deploys approximately 96 resources — all inside your own AWS account. Nothing is created in STELR's account.
What gets deployed:
- Two KMS keys: RSA 4096 for signing (
alias/stelr/customer) and AES-256 for ledger encryption (alias/stelr-ledger/customer) - DynamoDB governance ledger with three GSIs
- S3 WORM bucket (Object Lock, Compliance mode) — contents are immutable for the retention period
- Step Functions Express state machine (Provenance → Policy → Ledger Write)
- API Gateway with Lambda token authorizer using
X-API-Key - Eight Lambda functions: generation, verification, async pipeline, key registrar, and support functions
- IAM deny policy blocking direct Bedrock access + CloudTrail bypass detection via EventBridge → SNS
During deployment
The stack takes 3–6 minutes. Status cycles through CREATE_IN_PROGRESS for each resource. The KMS keys, S3 bucket, and DynamoDB table all provision before the Lambda functions. Refresh the CloudFormation console to track progress. When the status reaches CREATE_COMPLETE, proceed to Step 5.
Required IAM permissions (expand if you use permission boundaries)
Create a CloudFormation service role with this policy. The most common deployment failure is missing iam:CreateRole — the stack creates an execution role for every Lambda.
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "CloudFormation", "Effect": "Allow",
"Action": ["cloudformation:CreateStack","cloudformation:DeleteStack",
"cloudformation:DescribeStacks","cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResource","cloudformation:DescribeStackResources",
"cloudformation:GetTemplate","cloudformation:ValidateTemplate",
"cloudformation:UpdateStack"],
"Resource": "*" },
{ "Sid": "IAM", "Effect": "Allow",
"Action": ["iam:CreateRole","iam:DeleteRole","iam:GetRole",
"iam:PutRolePolicy","iam:DeleteRolePolicy","iam:GetRolePolicy",
"iam:AttachRolePolicy","iam:DetachRolePolicy",
"iam:ListAttachedRolePolicies","iam:ListRolePolicies","iam:PassRole",
"iam:CreatePolicy","iam:DeletePolicy","iam:GetPolicy",
"iam:CreatePolicyVersion","iam:DeletePolicyVersion","iam:ListPolicyVersions",
"iam:TagRole","iam:UntagRole","iam:TagPolicy","iam:UntagPolicy"],
"Resource": "*" },
{ "Sid": "KMS", "Effect": "Allow",
"Action": ["kms:CreateKey","kms:DescribeKey","kms:GetKeyPolicy","kms:PutKeyPolicy",
"kms:EnableKeyRotation","kms:GetKeyRotationStatus",
"kms:ScheduleKeyDeletion","kms:CancelKeyDeletion",
"kms:CreateAlias","kms:DeleteAlias","kms:UpdateAlias","kms:ListAliases",
"kms:TagResource","kms:UntagResource","kms:ListResourceTags"],
"Resource": "*" },
{ "Sid": "S3", "Effect": "Allow",
"Action": ["s3:CreateBucket","s3:DeleteBucket","s3:GetBucketLocation",
"s3:PutBucketPolicy","s3:GetBucketPolicy","s3:DeleteBucketPolicy",
"s3:PutBucketVersioning","s3:GetBucketVersioning",
"s3:PutEncryptionConfiguration","s3:GetEncryptionConfiguration",
"s3:PutObjectLockConfiguration","s3:GetObjectLockConfiguration",
"s3:PutLifecycleConfiguration","s3:GetLifecycleConfiguration",
"s3:PutBucketPublicAccessBlock","s3:GetBucketPublicAccessBlock",
"s3:PutBucketTagging","s3:GetObject"],
"Resource": "*" },
{ "Sid": "DynamoDB", "Effect": "Allow",
"Action": ["dynamodb:CreateTable","dynamodb:DeleteTable",
"dynamodb:DescribeTable","dynamodb:UpdateTable",
"dynamodb:UpdateTimeToLive","dynamodb:DescribeTimeToLive",
"dynamodb:DescribeContinuousBackups","dynamodb:UpdateContinuousBackups",
"dynamodb:TagResource","dynamodb:UntagResource"],
"Resource": "*" },
{ "Sid": "Lambda", "Effect": "Allow",
"Action": ["lambda:CreateFunction","lambda:DeleteFunction",
"lambda:GetFunction","lambda:GetFunctionConfiguration",
"lambda:UpdateFunctionCode","lambda:UpdateFunctionConfiguration",
"lambda:AddPermission","lambda:RemovePermission",
"lambda:CreateEventSourceMapping","lambda:DeleteEventSourceMapping",
"lambda:GetEventSourceMapping","lambda:UpdateEventSourceMapping",
"lambda:PublishLayerVersion","lambda:DeleteLayerVersion",
"lambda:GetLayerVersion","lambda:AddLayerVersionPermission",
"lambda:RemoveLayerVersionPermission",
"lambda:TagResource","lambda:UntagResource"],
"Resource": "*" },
{ "Sid": "StepFunctions", "Effect": "Allow",
"Action": ["states:CreateStateMachine","states:DeleteStateMachine",
"states:DescribeStateMachine","states:UpdateStateMachine",
"states:TagResource","states:UntagResource"],
"Resource": "*" },
{ "Sid": "ApiGateway", "Effect": "Allow",
"Action": ["apigateway:POST","apigateway:GET","apigateway:PUT",
"apigateway:DELETE","apigateway:PATCH"],
"Resource": "*" },
{ "Sid": "EventBridgeSNSSQS", "Effect": "Allow",
"Action": ["events:CreateEventBus","events:DeleteEventBus","events:DescribeEventBus",
"events:PutRule","events:DeleteRule","events:DescribeRule",
"events:PutTargets","events:RemoveTargets","events:ListTargetsByRule",
"events:TagResource","events:UntagResource",
"sns:CreateTopic","sns:DeleteTopic","sns:GetTopicAttributes",
"sns:SetTopicAttributes","sns:AddPermission","sns:RemovePermission",
"sns:TagResource","sns:UntagResource",
"sqs:CreateQueue","sqs:DeleteQueue","sqs:GetQueueUrl",
"sqs:GetQueueAttributes","sqs:SetQueueAttributes",
"sqs:TagQueue","sqs:UntagQueue"],
"Resource": "*" },
{ "Sid": "CloudWatchLogs", "Effect": "Allow",
"Action": ["logs:CreateLogGroup","logs:DeleteLogGroup",
"logs:PutRetentionPolicy","logs:DescribeLogGroups",
"logs:TagLogGroup","logs:UntagLogGroup",
"logs:CreateLogDelivery","logs:DeleteLogDelivery",
"logs:PutResourcePolicy","logs:DescribeResourcePolicies"],
"Resource": "*" }
]
}
Find your endpoint — CloudFormation Outputs tab
After CREATE_COMPLETE, open the stack in the CloudFormation console and click the Outputs tab. Your stack produces eight outputs:
| Output key | What it is |
|---|---|
| ApiEndpoint | This is the base URL for every API call. Format: https://<id>.execute-api.<region>.amazonaws.com/customer/. Note the trailing slash — strip it before appending a path (see Step 7). |
| SigningKeyArn | ARN of your RSA 4096 KMS signing key (alias/stelr/customer). The key registrar Lambda publishes the public half to STELR's registry. |
| LedgerTableName | DynamoDB governance ledger (StelrLedger-customer). Append-only; hash-chained. The delete() path raises NotImplementedError by design. |
| TrustLedgerBucket | S3 WORM bucket (stelr-trust-ledger-customer-<account-id>). Object Lock Compliance mode — contents cannot be modified or deleted for the retention period. |
| BedrockDenyPolicyArn | IAM managed policy that blocks bedrock:InvokeModel and related actions for any principal except STELR's execution roles. Attach to workforce roles (optional but recommended for governance completeness). |
| SecurityAlertsTopicArn | SNS topic that fires when CloudTrail detects a Bedrock call from a non-STELR principal. Subscribe your SIEM or security email. |
| PipelineArn | ARN of the Step Functions Express state machine. Every sync generation starts this pipeline synchronously (Provenance → Policy → Ledger Write). Useful for CloudWatch monitoring. |
| StelrApiEndpoint… | CDK-generated duplicate of ApiEndpoint. Same URL. Ignore this one. |
Copy the ApiEndpoint value — you'll use it in Step 7.
Register your signing key
Manual step — required once
The stack deploys the key registrar Lambda but does not invoke it automatically. Run this once after the stack reaches CREATE_COMPLETE.
It retrieves your KMS public key, registers it with STELR's registry, and seeds your API key into your stack's DynamoDB table.
aws lambda invoke \ --function-name stelr-key-registrar-customer \ --region <YOUR-DEPLOYMENT-REGION> \ --profile <YOUR-AWS-PROFILE> \ response.json
Expected output — and why it shows an error
{"StatusCode": 200, "FunctionError": "Unhandled", "ExecutedVersion": "$LATEST"}
The FunctionError is expected when the Lambda is invoked directly rather than via CloudFormation.
The Lambda completes the registration work (key → STELR's registry, API key → your DynamoDB) and then fails trying to signal CloudFormation — which isn't involved in a direct invocation.
The error does not mean the registration failed.
Verify success in CloudWatch Logs:
aws logs filter-log-events \
--log-group-name /aws/lambda/stelr-key-registrar-customer \
--filter-pattern "key_registered" \
--region <YOUR-DEPLOYMENT-REGION> \
--profile <YOUR-AWS-PROFILE> | jq '.events[].message | fromjson | {message, key_id}'
A successful run produces two log entries: key_registered (key is in STELR's registry) and api_key_seeded (credentials are ready). If neither appears, the SSM parameters from Step 3 were likely missing — create them and re-invoke.
Prove it works — sign a generation, then verify it
Set two variables from your CloudFormation Outputs. The ApiEndpoint value includes a trailing slash — the ${STELR_API%/} syntax strips it.
# From CloudFormation Outputs tab STELR_API="https://<YOUR-API-ID>.execute-api.<REGION>.amazonaws.com/customer" STELR_KEY="<YOUR-API-KEY>"
POST /generate — invoke Bedrock and sign the output
RESPONSE=$(curl -s -X POST "${STELR_API%/}/generate" \
-H "X-API-Key: $STELR_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Reply in one sentence confirming STELR is running.", "max_tokens": 64}')
echo "$RESPONSE" | jq '{content_id, output, signature_id, signed_at}'
HTTP 201 — successful response shape:
{
"content_id": "f4a91b2c-3d5e-4f78-9a01-b2c3d4e5f678",
"output": "STELR is running and your AI output has been cryptographically signed.",
"model_id": "us.anthropic.claude-haiku-4-5-20251001-v1:0",
"provider": "bedrock",
"signature_id": "f4a91b2c-3d5e-4f78-9a01-b2c3d4e5f678",
"signature": "eyJhbGciOiJSUzI1NiJ9...",
"output_hash": "a3b4c5d6e7f8...",
"signed_at": "2026-09-30T14:23:11.042Z",
"s3_key": "tenant/f4a91b2c.../ledger.json",
"chain_position": 1,
"correlation_id": "c1d2e3f4-..."
}
content_id and signature_id are the same UUID for synchronous generation — use either to verify.
POST /verify/{content_id} — verify the signature against the output
# Extract the values from the generate response
CONTENT_ID=$(echo "$RESPONSE" | jq -r .signature_id)
curl -s -X POST "${STELR_API%/}/verify/$CONTENT_ID" \
-H "X-API-Key: $STELR_KEY" \
-H "Content-Type: application/json" \
-d "{\"content\": $(echo "$RESPONSE" | jq '.output')}" \
| jq '{valid, found, tampered, signed_at, verified_at}'
HTTP 200 — successful verification:
{
"valid": true,
"found": true,
"tampered": false,
"signed_at": "2026-09-30T14:23:11.042Z",
"verified_at": "2026-09-30T14:23:17.389Z"
}
"valid": true — your stack is fully operational. The RSA 4096 KMS signature verified, the SHA-256 hash matched, and the ledger entry was located.
Full API surface
| Endpoint | Body | Returns |
|---|---|---|
| POST /generate | {"prompt":"...","max_tokens":1024,"model_id":"...","metadata":{}}prompt required; model_id optional (default: us.anthropic.claude-haiku-4-5-20251001-v1:0) |
201 — output + signature_id |
| POST /generate/async | Same shape as /generate |
202 — content_id immediately; poll GET /generate/status/{content_id} |
| POST /verify/{content_id} | {"content":"<exact AI output text>"} |
200 — {"valid":true,"found":true,"tampered":false,...} |
| POST /verify or /verify/batch |
[{"content_id":"...","content":"..."}] — JSON array, up to 100 items |
200 — {"results":[...],"summary":{...}} |
X-API-Key: <your-api-key> header. The authorizer validates the key's SHA-256 hash against your DynamoDB ledger — the raw key is never stored. Request bodies (including prompts) are never logged to CloudWatch (data_trace_enabled=false on all stages).
Troubleshooting
Stack creation fails: not authorized to perform or AccessDenied
The stack rolls back to ROLLBACK_COMPLETE immediately or during resource creation
Stack creation fails: not authorized to perform or AccessDenied
The stack rolls back to ROLLBACK_COMPLETE immediately or during resource creation
The IAM principal launching the stack is missing one or more of the required permissions. The most common culprits in order of frequency:
iam:CreateRole/iam:PassRole— the stack creates an execution role for every Lambda. This is usually the first permission that enterprise permission boundaries restrict.kms:CreateKey— required for both the signing CMK and the ledger encryption CMK.s3:PutObjectLockConfiguration— required to enable Object Lock on the trust ledger bucket; this permission is often omitted from S3 permission sets.
Create a CloudFormation service role with the full policy from Step 4 above, pass it as the IAM role when launching the stack, and redeploy.
Key registrar returns "FunctionError": "Unhandled" with "errorMessage": "'ResponseURL'"
response.json contains a KeyError stack trace instead of a key_id
Key registrar returns "FunctionError": "Unhandled" with "errorMessage": "'ResponseURL'"
response.json contains a KeyError stack trace instead of a key_id
This is expected behavior for manual invocation. The Lambda registers the key and seeds the API credentials, then tries to signal CloudFormation via a pre-signed ResponseURL — which doesn't exist in a direct invoke event.
The registration completes before the error occurs.
To confirm the registration worked, check CloudWatch Logs for both log entries:
aws logs filter-log-events \ --log-group-name /aws/lambda/stelr-key-registrar-customer \ --filter-pattern '"key_registered"' \ --region <YOUR-REGION> --profile <YOUR-PROFILE>
If key_registered or api_key_seeded are absent, the SSM parameters were missing when the Lambda ran. Create them (Step 3) and invoke the Lambda again — registration and seeding are both idempotent.
API calls return 401 or 403
curl returns {"message":"Unauthorized"} or the authorizer policy denies the call
API calls return 401 or 403
curl returns {"message":"Unauthorized"} or the authorizer policy denies the call
Three distinct causes:
-
Wrong header name. The authorizer reads
X-API-Key— notAuthorization. Confirm your curl command uses-H "X-API-Key: $STELR_KEY". -
API key not seeded. The key registrar Lambda reads
/stelr/api-keyfrom SSM and writes a hashed record to DynamoDB. If that SSM parameter didn't exist when the Lambda ran, the DynamoDB record was never written. Create the SSM parameter (Step 3) and re-invokestelr-key-registrar-customer. -
Key value mismatch. The authorizer stores
sha256(raw_key), not the raw key. If you stored a different value in SSM than what you're sending in the header, the hashes won't match. Confirm the exact value in/stelr/api-keymatches the key you're using in the header.
/generate returns 502 with BEDROCK_ERROR — AccessDeniedException
The Bedrock invocation fails even though the stack deployed successfully
/generate returns 502 with BEDROCK_ERROR — AccessDeniedException
The Bedrock invocation fails even though the stack deployed successfully
The default model (us.anthropic.claude-haiku-4-5-20251001-v1:0) is a cross-region inference profile. It routes calls through multiple regions, so you need to enable model access in each region the profile uses — not just your deployment region.
In the AWS console:
- Open Amazon Bedrock in the same region as the stack.
- Go to Model access in the left sidebar.
- Request access to Claude Haiku 4.5 (Anthropic).
- Repeat in
us-west-2if your deployment is inus-east-1, and vice versa — theus.*cross-region profile can route to either.
To use a different model, pass "model_id" in your generate request body with any Bedrock model ID you have access to.
Still stuck?
Email support@stelrai.com with your STELR Customer ID, the CloudFormation stack name, your AWS region, and a brief description of the error. Include the correlation_id from any failed API response if you have it.