OrkStageOrkStage

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_KEY and AWS_REGION in the environment of the API and the worker (the .env of the production Compose file).
  • An AWS CLI profile on the development stack: AWS_PROFILE=<name> pnpm platform:aws-real in the development workspace mounts your ~/.aws read 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.

ParameterValue
ControlPlanePrincipalArnARN of the identity the OrkStage worker runs as, for example arn:aws:iam::111122223333:role/orkstage-worker.
ExternalIdA random shared secret of at least 16 characters (openssl rand -hex 24). Marked NoEcho.
RoleNameDefaults to orkstage-deploy.
MaxSessionDurationDefaults 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:SimulatePrincipalPolicy on 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 worker

See 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 in stateBucket on 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=true

Register 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 fieldMeaning
accountIdThe 12 digit account id (the AccountId output).
roleArnThe RoleArn output.
stateBucketOptional. Defaults to orkstage-tfstate-<accountId>-<region>.
dnsZonePlatform DNS zone for instance subdomains. It must already exist in Route 53.
ecsAmiIdOptional 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:

  1. resolves the external id and assumes the role;
  2. checks that the credentials belong to accountId;
  3. checks that the state bucket is reachable;
  4. 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:

  1. Update the role stack with the new ExternalId. From then on, only the new value works.
  2. Store the new value under a new secret name, for example orkstage/aws/prod-external-id-2.
  3. 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 resourceAWS resources
networkVPC, public and private subnets, internet gateway, route tables, internal security group, optional NAT gateways
container-registryECR repository, lifecycle policy
compute.container-serviceECS 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.postgresqlRDS PostgreSQL instance, subnet group, security group, Secrets Manager secret (the password is generated as an ephemeral value and passed through write only attributes)
cache.redisElastiCache Valkey replication group, subnet group, security group
queueSQS queue and dead letter queue
object-storageS3 bucket with public access block, encryption, versioning and lifecycle rules
ingressApplication Load Balancer, target groups, HTTP listener and rules
dnsRoute 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>.

On this page