How to deploy sops-aws-sync with GitHub Actions¶
Wire the Action into a workflow that plans on pull requests and syncs on protected merges, with AWS OIDC authentication, single-writer concurrency, and a pinned, provenance-verified CLI.
Prerequisites¶
- An AWS role assumable from GitHub via OIDC, carrying the least-privilege Secrets Manager and KMS policy from How to grant sops-aws-sync least-privilege AWS access. Use a read-only role for the plan job and a write-capable role for the sync job.
- The GitHub-to-AWS OIDC trust relationship already established. Setting up the OIDC provider and role trust policy is generic AWS/GitHub work — follow GitHub's OpenID Connect guide for Amazon Web Services and do not re-derive it here.
- SOPS decryption keys reachable from the runner (age/KMS/PGP/Vault, resolved by SOPS from its own environment and each file's metadata).
- Committed
*.sops.json,*.sops.yaml, or*.sops.ymlfiles laid out under a source root, per the Reconciliation reference. - A chosen, frozen ownership scope — the
repository-id,source-root, andsecret-prefixtriple. Treat these as an immutable identity; changing any of them silently abandons the secrets under the old scope. See About ownership, scope, and fail-closed safety. - The full release commit SHA to pin the Action to (the commit behind the exact release tag you intend to run).
The Action verifies the CLI binary's SHA-256 checksum and SLSA provenance automatically on every install and every tool-cache hit, so you never perform manual verification in CI. For direct (non-Action) CLI use, see How to verify a downloaded release binary; for what that verification proves, see About the security and trust model.
Wire the shared building blocks¶
The plan and sync workflows share the same five decisions. Apply all of them in both.
1. Lock down workflow permissions¶
Set no permissions at the workflow top level and grant the minimum per job:
permissions: {}
jobs:
reconcile:
permissions:
contents: read # read the committed SOPS document blobs
id-token: write # mint the GitHub OIDC token for AWS
attestations: read # read a private repo's CLI attestations
contents: read is always required. id-token: write is required only when
AWS authentication uses OIDC (the recommended path). attestations: read is
required only when provenance verification must read a private repository's
attestations; the default ${{ github.token }} verifies a public same-repo
install without it, and the scope is harmless when unused.
2. Authenticate to AWS first¶
Add the AWS credential step before the sops-aws-sync step. The Action
performs no AWS authentication of its own — it forwards only --aws-region and
relies on the ambient AWS SDK credential chain the runner already holds. Missing
or insufficient credentials therefore surface later as a CLI runtime failure
(an ownership/observed-state conflict or an apply failure), not as an Action
input error. See How to diagnose and recover from a failed run.
- uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.SOPS_AWS_SYNC_ROLE_ARN }}
aws-region: us-west-2
The Action never receives credential material. It accepts no access key, secret
key, or session token input — only --aws-region is forwarded.
3. Check out the exact revision¶
Check out with full history and without leaving Git credentials on disk, so the committed blobs at the resolved revision are present:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
The Action defaults revision to GITHUB_SHA and repository-id to
GITHUB_REPOSITORY. The tool reads the committed blob at the resolved revision,
never the working tree — the checkout must contain that commit.
4. Pin the Action to a release commit SHA¶
Reference the Action by the full commit SHA behind an exact release tag, with a
version comment. Do not use @v1, @master, or any floating ref:
Leave cli-version unset. Each pinned release ref carries the exact CLI version
it was built and tested against as its default. Overriding cli-version to a
value that does not match the pinned ref decouples the CLI from the Action and
can fail installation or trip the CLI exit status and redacted report disagree
error. The CLI and the Action move in lockstep — see
About the security and trust model for
why pinning is a trust and correctness control.
5. Serialize writers per scope¶
The tool assumes exactly one active writer per ownership scope; AWS offers no
cross-secret transaction to fall back on. Enforce this with a concurrency
group keyed to the scope (not to the branch or PR), and never cancel a run in
flight:
Run sync only from a protected default-branch merge or a protected-environment
deployment — never from pull_request code holding AWS credentials. For why the
single-writer assumption is load-bearing, see
About consistency, verification, and recovery.
Variation A — plan on pull requests with drift gating¶
Run a read-only plan on pull requests and fail the check when executable drift
is found, gating the merge. fail-on-drift is valid only in plan mode and makes
drift return exit code 2. Boolean inputs must be exactly lowercase true or
false.
# .github/workflows/sops-plan.yml
name: sops-aws-sync plan
on:
pull_request:
paths:
- secrets/**
permissions: {}
concurrency:
group: sops-aws-sync-plan-${{ github.ref }}
cancel-in-progress: true
jobs:
plan:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.SOPS_AWS_SYNC_PLAN_ROLE_ARN }}
aws-region: us-west-2
- id: plan
uses: meigma/sops-aws-sync@<full-release-commit-sha> # vX.Y.Z
with:
mode: plan
fail-on-drift: true
source-root: secrets
secret-prefix: /example/production
aws-region: us-west-2
Give the plan job a read-only AWS role and restrict it to same-repository pull requests — GitHub does not issue an OIDC token to forked-PR workflows, and a read-only role limits the blast radius of PR code. A converged plan passes; a drift plan exits 2 and fails the check.
Variation B — sync on protected merges¶
Run sync on a push to the protected default branch. Do not set
fail-on-drift in sync mode — combining the two fails input parsing with
Input fail-on-drift is valid only in plan mode. Gate downstream steps on the
status output.
# .github/workflows/sops-sync.yml
name: sops-aws-sync sync
on:
push:
branches: [main]
paths:
- secrets/**
permissions: {}
concurrency:
group: sops-aws-sync-production
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
attestations: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.SOPS_AWS_SYNC_ROLE_ARN }}
aws-region: us-west-2
- id: sync
uses: meigma/sops-aws-sync@<full-release-commit-sha> # vX.Y.Z
with:
mode: sync
source-root: secrets
secret-prefix: /example/production
aws-region: us-west-2
- name: Continue only on a converged sync
if: steps.sync.outputs.status == 'converged'
run: |
echo "status=${{ steps.sync.outputs.status }}"
echo "verification=${{ steps.sync.outputs.verification-status }}"
The environment: production binds the job to a protected environment; configure
its protection rules (required reviewers, deployment branch restrictions) to
approve privileged syncs. cancel-in-progress: false guarantees a running sync
is never cancelled mid-flight — an interrupted sync must be re-run; see
How to diagnose and recover from a failed run.
Variation C — private or cross-repository installs¶
For a public same-repository install, the default github-token (${{
github.token }}) is sufficient and needs no override. For a private or
cross-repository consumer, supply a token that can read the sops-aws-sync
repository's contents and attestations:
- id: sync
uses: meigma/sops-aws-sync@<full-release-commit-sha> # vX.Y.Z
with:
mode: sync
secret-prefix: /example/production
github-token: ${{ secrets.SOPS_AWS_SYNC_READ_TOKEN }}
This token authenticates release-metadata reads, asset download, and
gh attestation verify only. It is masked before any installation work, is
passed to gh through an isolated configuration directory, and is never
forwarded to AWS. Do not reuse an AWS-privileged token here.
Variation D — self-hosted and container runners¶
The Action runs on the node24 runtime and installs the CLI only for
Linux/macOS on X64/ARM64 runners. A self-hosted or container runner must
therefore provide:
- Node 24 support for the Action runtime.
- A supported platform:
LinuxormacOS,X64orARM64. Other operating systems or architectures fail with an explicit "supports only Linux and macOS" / "only X64 and ARM64" message. - A
ghCLI present and new enough to advertise the attestation policy flags the Action probes; an old or missingghfails with "A compatible GitHub CLI is required for attestation verification." Installingghis out of scope — follow the upstream GitHub CLI documentation. - The runner environment variables the Action reads:
GITHUB_WORKSPACE,RUNNER_TEMP,RUNNER_OS,RUNNER_ARCH,GITHUB_SHA, andGITHUB_REPOSITORYare all always required. Supplying therevisionandrepository-idinputs does not remove the requirement forGITHUB_SHAandGITHUB_REPOSITORY. Container images that strip any of these fail before installation.
The full enumeration of inputs, outputs, defaults, and validation rules is in the Configuration reference.
Consume the results¶
The step publishes a fixed set of outputs sourced only from the redacted machine
report: status, cli-version, git-revision, create-count,
update-count, restore-count, scheduled-delete-count, unchanged-count,
conflict-count, verification-status, and redacted-report-path. Read them
from steps.<id>.outputs.<name> to gate or annotate downstream work, as the
sync workflow does above.
The Action also writes a two-table job summary built only from the validated report — the first table covering status, CLI version, git revision, and verification, the second the six operation counts — safe to leave visible.
To retain the redacted report as a build artifact, upload it from
redacted-report-path within the same job (the file lives under RUNNER_TEMP
and is cleared when the job ends):
- name: Archive the redacted report
if: always()
uses: actions/upload-artifact@<full-commit-sha> # v4 — pin to a SHA
with:
name: sops-aws-sync-report
path: ${{ steps.sync.outputs.redacted-report-path }}
The report and default logs carry no secret values, names, or paths, so they are
safe to archive. Because raw CLI stderr is suppressed to a single fixed warning
(sops-aws-sync wrote a diagnostic to stderr), debug from the JSON stdout log
lines and the redacted report — not from stderr. The output and report field
lists are catalogued in the Configuration reference
and Results and exit codes reference.
Verify the deployment¶
A clean sync reports status: converged and verification-status: converged,
and the step succeeds (exit 0). Confirm both outputs on a merge run:
- name: Assert convergence
run: |
test "${{ steps.sync.outputs.status }}" = converged
test "${{ steps.sync.outputs.verification-status }}" = converged
A converged scope is idempotent (see
About the reconciliation model) — a
green pipeline that reports unchanged-count equal to your secret count and
zero mutations confirms the scope is healthy. Any other status or a non-zero exit means the run did not
converge — go to
How to diagnose and recover from a failed run.
Related¶
- How to grant sops-aws-sync least-privilege AWS access
- How to verify a downloaded release binary
- How to diagnose and recover from a failed run
- Configuration reference
- Results and exit codes reference
- About the reconciliation model
- About ownership, scope, and fail-closed safety
- About consistency, verification, and recovery
- About the security and trust model