Security model
No single compromise reaches infrastructure. The server has no cloud access, the runner has no server secrets, and GitHub's own controls gate the one action that changes anything.
What each party can do if compromised
| Compromised | Can | Cannot |
|---|---|---|
| Stackorder server | Dispatch stackorder-run.yml in installed repos, post checks and comments, read stackorder.yaml, read plan summaries and capped plan text, answer its own deployment protection rule where one is configured | Read or write Terraform state, assume any AWS role, change workflow files, read repo secrets, approve pull requests, pass an environment's required reviewers |
| A PR author with write access | Trigger plans on their PR, comment stackorder apply if policy allows; add a stack or instance that nothing on the default branch maps, so that an instance applies under an unprotected environment of its own name and a stack without instances under default, which has no reviewers; only an IAM trust policy pinned to the environment stops such an apply | Bypass required approvals, branch protection or GitHub environment reviewers; skip policy checks recorded on the stack |
| A modified workflow in a PR | Change what runs in the plan job on that PR | Post results the server accepts, when STACKORDER_REQUIRED_WORKFLOW_REF pins the reusable workflow; assume the AWS role, when the role's trust policy pins job_workflow_ref or the environment |
| A leaked App private key | Everything the server can | Everything the server cannot; rotate in the App settings and redeploy |
Controls that belong to GitHub
Stackorder reports; GitHub and AWS enforce.
- Branch protection requires the
stackorder/plancheck, thestackorder/applycheck inbefore_mergemode, and the configured approvals. GitHub enforces the merge. - GitHub Environments with required reviewers on the apply job add a human gate the server cannot skip, because the server cannot approve deployments.
- The AWS role trust policy restricts
subtorepo:org/repo:environment:productionfor an apply role, and optionally to thejob_workflow_refof the canonical reusable workflow, so only the gated job in that repository can obtain credentials. The read-only plan role trustsrepo:org/repo:pull_requestandrepo:org/repo:environment:default. See Security hardening. - Runner OIDC tokens are short-lived and bound to one run. There is nothing to rotate on the runner side.
The five layers that gate an apply, and which of them are real security boundaries, are on Environments and authorization.
Trust between runner and server
There are no shared secrets between the runner and the server. The CLI sends the job's GitHub OIDC token, requested with the server's base URL as audience. The server verifies its signature against GitHub's keys and binds it to the run with its claims: repository and repository id, event and ref, workflow run, environment and, optionally, job_workflow_ref. It never compares the token's sha claim, which is GitHub's merge or dispatch commit rather than the commit being planned; it checks the pull request head through the GitHub API instead. See OIDC binding.
Rotating anything means rotating the App's private key, which runners never see.
What crosses each boundary
| Boundary | What crosses it |
|---|---|
| GitHub to server | Webhook payloads (HMAC-signed); from runners, JSON manifests and result summaries. No repository contents beyond file paths and parsed dependency edges. |
| Server to GitHub | App installation tokens, scoped to one installation and valid for an hour, used for workflow_dispatch, check runs and comments. |
| Runner to AWS | Your role, assumed with aws-actions/configure-aws-credentials and a trust policy pinned to the repository and environment. State, lock and plan files never leave this zone. |
| Server to anything else | Nothing, except Postgres, GitHub's OIDC signing keys, and, when enabled, the artifact bucket and the OTLP endpoint. |
Secrets in plan output
- Terraform and OpenTofu already mask values marked
sensitive. - The CLI redacts everything it sends to the server, the step summary or a fallback check: private keys, tokens, password assignments and the values of secret-named environment variables. In Actions it also registers those values with
::add-mask::. See CLI secrets. - Plan text sent to the server is truncated at 256 KB.
- The API returns that redacted text, from Postgres or from the artifact bucket, only to people who can see the repository, to API keys and to the jobs of the run itself.
plan_output: summary, per repository or per stack, sends only resource counts and addresses to the server and the PR comment.
The full plan lives only in the job log and the plan artifact, both governed by the repository's own access rules and artifact retention.
Data retention
Plan text is kept 30 days by default, summaries and run history indefinitely, queue events 7 days and drift history 90 days. The periods are set with STACKORDER_PLAN_TEXT_RETENTION, STACKORDER_EVENT_RETENTION and STACKORDER_DRIFT_RETENTION. See Data model.
Availability
The server is not in the path of terraform plan. A server outage degrades to plans with unconfirmed checks and refused applies. Postgres is the only stateful dependency; point-in-time recovery on RDS covers it. If GitHub webhooks are delayed, the reconciliation that runs every minute catches finished waves. If GitHub Actions is down, nothing runs, exactly as with any Actions-based tool.
Abuse limits
- Comment commands are accepted only from users with push permission, and rate-limited to 10 per minute per PR.
- Webhook deliveries are deduplicated by delivery id.
- The API refuses OIDC tokens issued more than 10 minutes ago and tokens it has already seen, results for stacks that already finished or runs that were superseded, and tokens from a workflow run whose dispatch already completed or is bound to another workflow run.
Human access
People sign in with GitHub through the App's OAuth client, with the read:org scope only. A session is issued only to a user whose own account, or one of whose organisations, has the App installed, and the UI shows only those accounts' repositories. API keys, created by an operator, see every repository. The UI is read-only except for unlock and re-run, which go through the API and are audited.