Connecting an AWS account
Create the OrkStage deployment role with an external id, prepare the OpenTofu state bucket, then register and validate an AWS account as a cloud target.
OrkStage never stores AWS keys for your accounts. In each account, the worker assumes a deployment
role with AssumeRole and an external id. It receives short lived credentials that last one hour
(AWS_ROLE_SESSION_MINUTES), stay in memory and are never persisted or logged. Every OpenTofu run gets
credentials that outlive its time limit, so a long apply never runs out halfway.
control plane identity (worker task role / IAM user)
│ sts:AssumeRole + ExternalId
▼
orkstage-deploy role in the customer account ──▶ OpenTofu runner (credentials for that one container)The deployment role can create and delete infrastructure. Use a dedicated AWS account for each environment.
Give OrkStage its own identity
The API and the worker call STS with their own AWS identity, found through the default credential chain. One identity serves every account you connect. Pick one:
- EC2 instance profile (recommended on AWS): attach a role to the instance that runs OrkStage.
Containers reach the instance metadata only with a hop limit of 2:
aws ec2 modify-instance-metadata-options --instance-id i-... --http-put-response-hop-limit 2 --http-tokens required. - IAM user keys anywhere else:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYandAWS_REGIONin the environment of the API and the worker (the.envof the production Compose file). - An AWS CLI profile on the development stack:
AWS_PROFILE=<name> pnpm platform:aws-realin the development workspace mounts your~/.awsread only.
The identity needs only infra/aws/orkstage-control-plane-policy.json from the platform repository:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "AssumeDeploymentRoles", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "arn:aws:iam::*:role/orkstage-deploy" },
{ "Sid": "ReadExternalIds", "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "arn:aws:secretsmanager:*:*:secret:orkstage/*" }
]
}Narrow sts:AssumeRole to the list of role ARNs when you know them, and drop the second statement
when you use env:// references only. Its ARN is the ControlPlanePrincipalArn of every role stack
below. Without an identity, Validate says so instead of reaching AWS.
Connect an account
Create the deployment role
Launch the CloudFormation template infra/aws/orkstage-deployment-role.yaml from the platform
repository once in each account you connect.
| Parameter | Value |
|---|---|
ControlPlanePrincipalArn | ARN of the identity the OrkStage worker runs as, for example arn:aws:iam::111122223333:role/orkstage-worker. |
ExternalId | A random shared secret of at least 16 characters (openssl rand -hex 24). Marked NoEcho. |
RoleName | Defaults to orkstage-deploy. |
MaxSessionDuration | Defaults to 3600 seconds. Raise it with AWS_ROLE_SESSION_MINUTES when OpenTofu runs may take longer than 55 minutes. |
aws cloudformation deploy \
--stack-name orkstage-deployment-role \
--template-file infra/aws/orkstage-deployment-role.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
ControlPlanePrincipalArn=arn:aws:iam::111122223333:role/orkstage-worker \
ExternalId="$EXTERNAL_ID"The stack outputs RoleArn and AccountId. You paste both into the cloud target.
Use a different external id in each account. With many accounts in one AWS Organization, the same template works as a CloudFormation StackSet, with the external id as a parameter override per account.
In short, the role may:
- manage EC2 and VPC, ECS, RDS, ElastiCache, ELB, Auto Scaling, Route 53 and ACM resources;
- manage ECR repositories, SQS queues, Secrets Manager secrets, S3 buckets and IAM service roles
whose names start with
ork-(every physical name OrkStage creates does), plus log groups under/orkstage/; - read and write the OpenTofu state bucket
orkstage-tfstate-<account>-*; - pass
ork-*roles to ECS tasks and EC2 only; - call
iam:SimulatePrincipalPolicyon itself, so OrkStage can check permissions before it deploys.
The template tracks the actions the OpenTofu modules need. A platform unit test fails when a module needs an action that the policy does not grant.
Store the external id
Put the external id in a secret store that the control plane can read, and give OrkStage a reference to it. Never paste the value itself.
aws-sm://orkstage/aws/prod-external-id # AWS Secrets Manager (name or ARN, optional #jsonKey)
env://ORKSTAGE_AWS_PROD_EXTERNAL_ID # environment variable of the API and workerSee Secret references.
Prepare the state bucket
OpenTofu state lives in S3, with one state file per resource (orkstage/<physical-name>.tfstate). In
a real account, the bucket must exist before the first deployment and must have:
- the default name
orkstage-tfstate-<accountId>-<region>, or the name you set instateBucketon the target; - versioning enabled, so you can recover from a damaged state;
- default encryption enabled;
- public access blocked.
Locking uses the lock file that OpenTofu keeps in S3 (use_lockfile), so you do not need a DynamoDB
table.
B=orkstage-tfstate-123456789012-us-east-1
aws s3api create-bucket --bucket "$B" --region us-east-1
aws s3api put-bucket-versioning --bucket "$B" --versioning-configuration Status=Enabled
aws s3api put-bucket-encryption --bucket "$B" \
--server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
aws s3api put-public-access-block --bucket "$B" \
--public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=trueRegister the cloud target
Platform owners and admins can open Cloud targets in the console and click Register cloud target. You can also call the API:
POST /api/v1/cloud/targets
Content-Type: application/json
{
"provider": "aws",
"name": "aws-prod",
"environment": "production",
"config": {
"accountId": "123456789012",
"roleArn": "arn:aws:iam::123456789012:role/orkstage-deploy",
"stateBucket": "orkstage-tfstate-123456789012-us-east-1",
"dnsZone": "apps.example.com"
},
"credentialRef": "aws-sm://orkstage/aws/prod-external-id",
"allowedRegions": ["us-east-1", "eu-west-1"],
"defaultRegion": "us-east-1",
"maxConcurrentDeployments": 3
}config field | Meaning |
|---|---|
accountId | The 12 digit account id (the AccountId output). |
roleArn | The RoleArn output. |
stateBucket | Optional. Defaults to orkstage-tfstate-<accountId>-<region>. |
dnsZone | Platform DNS zone for instance subdomains. It must already exist in Route 53. |
ecsAmiId | Optional AMI for ecs-ec2 capacity. When empty, OrkStage uses the latest ECS optimized AMI published in SSM. |
config must not contain secrets. The API rejects keys that look like secrets with
422 SECRET_IN_CONFIG, and a role from another account than accountId with 422 INVALID_CONFIG. credentialRef must be an env:// or aws-sm:// reference, and any other
value is rejected. You can assign a target to one tenant (tenantId) or share it across tenants.
Validate
Click Validate, or call POST /cloud/targets/:id/validate { "region": "us-east-1" }. OrkStage
then:
- resolves the external id and assumes the role;
- checks that the credentials belong to
accountId; - checks that the state bucket is reachable;
- simulates the role against the actions each supported service needs, with
iam:SimulatePrincipalPolicy.
Missing actions for a service appear as warnings on the Validate button, because you may scope a
target to fewer services on purpose. If the role cannot call iam:SimulatePrincipalPolicy, the
permission check is reported as skipped.
Edit a target or rotate its credentials
Click Edit on the target, or call PATCH /api/v1/cloud/targets/:id. You can change the name, the
role ARN, the credential reference, the DNS zone, the regions and the concurrency limit. The account
id and the state bucket are fixed, because existing resources and their state live there: a
different account is a new target. A region that still has resources, instances or profiles cannot
be removed. Any change other than the name or the concurrency limit clears the last validation, so
run Validate again.
To rotate the external id without downtime:
- Update the role stack with the new
ExternalId. From then on, only the new value works. - Store the new value under a new secret name, for example
orkstage/aws/prod-external-id-2. - Edit the target and set the new reference, then Validate.
Deployments that start after the edit use the new reference at once; the old secret can go.
Several accounts
Register one target per account and environment, each with its own role and external id, and create one profile per environment that points at them (a tenant profile replaces the shared one for that tenant when a customer has its own account). Targets in different accounts deploy at the same time without sharing credentials: each OpenTofu container receives only its own target's.
During a deployment
The Validate cloud targets phase repeats these checks for each target and region the deployment
uses. This time it checks only the services being deployed, and it is strict. A missing permission
fails the phase with AUTHORIZATION_ERROR before any resource is touched, and the error names the
missing actions.
What OrkStage creates in your account
| Template resource | AWS resources |
|---|---|
network | VPC, public and private subnets, internet gateway, route tables, internal security group, optional NAT gateways |
container-registry | ECR repository, lifecycle policy |
compute.container-service | ECS cluster, task definition, service, log group, execution role, security group. With ecs-ec2: EC2 launch template, Auto Scaling group and capacity provider. With autoscaling: Application Auto Scaling. |
database.postgresql | RDS PostgreSQL instance, subnet group, security group, Secrets Manager secret (the password is generated as an ephemeral value and passed through write only attributes) |
cache.redis | ElastiCache Valkey replication group, subnet group, security group |
queue | SQS queue and dead letter queue |
object-storage | S3 bucket with public access block, encryption, versioning and lifecycle rules |
ingress | Application Load Balancer, target groups, HTTP listener and rules |
dns | Route 53 record, ACM certificate with DNS validation, HTTPS listener |
Every resource carries the platform's standard tags and a name of the form
ork-<tenant>-<instance>-<resource>.
Configuration reference
Every OrkStage environment variable with its type, default and effect, from core settings and OIDC to the OpenTofu runner, email, secrets and licensing.
Secret references
OrkStage stores references to secrets, not values. The env and AWS Secrets Manager schemes, JSON keys, emulator settings and where each secret lives.