Skip to content

About the security and trust model

sops-aws-sync runs with your AWS identity and handles your decrypted secrets. Those two facts are the whole security story. The binary you install will hold your credentials and your plaintext at the same time, so "can I trust this binary?" and "what actually stays confidential?" are not two separate questions — they are the same question asked from two directions. This document explains both boundaries: what the tool keeps confidential (and, just as importantly, what it does not), and how you can trust that the binary doing the handling is the one the project actually published.

Read this before you grant the tool AWS access. Everything here is about understanding the model well enough to reason about your own exposure; the procedures that act on it live in the how-to guides linked throughout.

What is actually secret

The most dangerous assumption an operator can bring to this tool is that everything inside a committed SOPS document is secret. It is not, and acting as though it is will eventually leak something you believed was protected.

SOPS encrypts values, not whole documents. It supports partial encryption, and the tool accepts that faithfully: it delegates decryption wholesale to SOPS and never asserts that every leaf was encrypted. Whatever your encryption policy left in cleartext stays in cleartext in the committed file. In practice, a committed SOPS document — a .sops.json, .sops.yaml, or .sops.yml file — exposes, to anyone who can read the repository:

  • the document structure itself — every key and member name;
  • any leaf your policy deliberately left unencrypted;
  • the SOPS metadata block, which can name key backends and identifiers such as a KMS ARN or an age recipient;
  • the file's path in the repository, which the tool also derives the secret's name from.

Only the encrypted values are confidential, and even those are confidential in Git only to the extent your repository's access controls make them so. The tool's confidentiality guarantees begin at the point of decryption and apply to what the tool does with the plaintext; they say nothing about what SOPS chose to leave visible in the file you committed. Treat the cleartext portions of a SOPS document as public to everyone with repository access, and design your key layout and repository permissions on that basis.

There is one integrity guarantee that does travel with the file. SOPS verifies the document's MAC as part of decryption, over the exact committed bytes. Tampered ciphertext fails to decrypt, and because the tool reads, decrypts, and validates the entire desired state before it issues a single AWS mutation, a tampered or corrupt document stops the run before anything is written.

How secrets are kept from leaking

Once a value is decrypted, it lives only in process memory. It is never written to the machine report, never placed on a command line, never put into an environment variable, and never logged. What the tool does not promise is to scrub that memory: Go's garbage-collected strings and byte slices, and the AWS SDK's own buffers, give no reliable way to erase plaintext from RAM while the process is alive. The guarantee is about egress — the channels through which a secret could escape — not about zeroing memory mid-run. For a long-lived host this is a threat-model boundary worth stating plainly; for the short-lived CI process the tool is designed to run in, it is a small residual.

The safety of those egress channels does not rest on a scrubber that hopes to catch every sensitive string on its way out. It is structural: sensitive text never enters the tool's error and log values in the first place.

  • AWS failures are reduced to a typed, safe error that carries only an operation name, a modeled error code, a fault class, and an AWS request id — never the raw SDK message, which can contain ARNs. The request id is the value you hand to AWS support; the rest stays out.
  • A decryption failure — wrong key, missing key, corrupt ciphertext, a bad MAC, or an unreachable KMS or age backend — collapses to a single fixed message. The underlying reason, which could name a key or a provider, is discarded rather than surfaced.
  • SOPS's own library logging, which can print provider identifiers such as a KMS ARN, is silenced entirely, independent of your log settings.
  • Operations log a run-local ordinal such as resource-0001 in place of the secret's name. Real names appear only if you deliberately opt in with --show-resource-names, and only then in a log sink you trust.
  • The machine report is an exact-key allowlist. It carries counts, statuses, versions, and durations; a field that is not on the list is rejected as an error rather than passed through silently.

None of this weakens at --log-level=debug. Debug logging adds detail about decisions and retries; it never adds plaintext. That is why the default logs and the report are safe to archive, attach to a ticket, or paste into a chat without a redaction pass of your own — a property the GitHub Action leans on when it forwards CLI output and publishes a summary.

It is worth being explicit about why the tool prefers this structural approach over a redaction filter, because it is a deliberate stance. A best-effort redactor is a denylist: it has to anticipate every shape of sensitive data and every place it might appear, and it fails open the first time it misses one. An allowlist fails closed: only the values explicitly permitted can appear, so a sensitive field added to the code tomorrow is excluded by default rather than by the redactor's luck. When the cost of a miss is a leaked secret, failing closed is the only defensible default. (The exact report fields and log vocabulary are catalogued in the results and configuration references; this section explains why they are shaped the way they are, not what each field is.)

Authentication is yours to establish

Neither the CLI nor the Action accepts credential material. There is no access-key flag, no secret-key flag, and no session-token input anywhere on the surface. Credentials come exclusively from the ambient AWS SDK chain that you set up before the tool runs — environment variables, shared config and profiles, SSO, IRSA, or instance and container roles. The only steering the tool offers is a region and a profile, and the Action forwards only a region.

The optional github-token is a key for an entirely different lock. It authenticates GitHub release metadata, asset download, and attestation verification — and nothing else. It is masked the instant it is read, handed to the GitHub CLI only inside an isolated configuration directory with the ambient GitHub-token environment variables stripped out first, and it is never forwarded to the CLI or to AWS. Its default is the workflow's own automatic token; you override it only for private or cross-repo installs.

The rule that falls out of this boundary is short and non-negotiable: never hand AWS credentials to a run you do not trust. In particular, never expose them to a pull_request-triggered workflow that carries code you have not reviewed. The tool executes with exactly the identity you give it and no more, which is why the strength and scope of that identity is a decision you own — and why the provenance of the binary consuming it is the next thing to get right. (Wiring the credential step, its OIDC permissions, and the single-writer concurrency it depends on is covered in deploy with GitHub Actions; the least-privilege policy itself is in grant AWS access.)

Trusting the binary: what "attested" proves

Because the binary you install will run with your AWS identity and your plaintext, "where did this binary come from?" is a security question, not a convenience. The Action answers it before it executes anything, by requiring two independent proofs.

First, the downloaded binary's SHA-256 must match its entry in the release's checksums.txt. Second — and this is the load-bearing one — gh attestation verify must confirm a signed SLSA provenance statement against a policy the Action pins in its own code: the artifact must have been built by this repository, by its own attest.yml signer workflow, for the exact release tag and its exact commit, with the GitHub Actions OIDC issuer as the certificate authority, as SLSA provenance v1, and on a GitHub-hosted rather than a self-hosted runner. A checksum on its own only proves a file matches a manifest; it says nothing about who produced the manifest. Provenance supplies the missing "who."

It is worth being precise about what each proof binds, because both bind the binary directly. The release build stamps every entry in checksums.txt as its own provenance subject — actions/attest with subject-checksums: checksums.txt expands each line into a subject pairing an asset name with its SHA-256, so each individual binary, not the manifest, is what the attestation covers. The Action therefore checks the binary twice against the same digest: a plain SHA-256 equality of the downloaded bytes against the manifest line for your asset, and a provenance check that runs gh attestation verify on the binary itself and requires a signed statement whose subject digest equals that same SHA-256. There is no hash-of-hashes indirection and no transitive trust through the manifest's own provenance; the checksum step proves the bytes match the manifest, and the provenance step proves those exact bytes were built by the pinned workflow. The Action re-runs both checks even on a tool-cache hit, so a cache entry poisoned between jobs on a shared runner cannot be trusted merely because it already exists. Any failure is terminal: the Action fails closed with a deliberately generic message, and the binary never runs. (The exact command and its flags are in verify a release binary; you only run that by hand when you install the CLI directly, since the Action does it for you.)

This is also why a release built by a fork, a re-signed binary, or an artifact from a moved or deleted tag cannot pass: none of them satisfies the pinned identity, ref, and issuer, and that is by design.

It is equally important to be clear about the limits of what an attestation proves. It establishes build identity and integrity — that this exact artifact was produced by this project's workflow for this commit. It does not prove the binary is free of bugs, it does not vouch for how the code behaves at runtime, and it does not attest the upstream dependency supply chain that fed the build. Provenance answers "is this the artifact the project published?", not "is the project's code correct?" Trusting the source is a prerequisite for, not a substitute for, your own judgement about the tool.

Version pinning as a trust control

Pinning is usually framed as a compatibility concern. Here it is also a security control. The CLI version is fully pinned and immutable — an exact semantic version, with no leading v, no ranges, and no "latest". The CLI and the Action move in lockstep: a given Action release embeds and installs exactly one matching CLI version, and the release process refuses to let the two drift apart.

Two things follow for operators. Pin the Action to the full commit SHA behind a release tag, not to a floating tag such as @v1 or @master. A floating tag is a mutable pointer; a future release, or a compromise of the repository, can move it under you, and moving it is exactly the attack a SHA pin removes — the pinned commit either is or is not the one you reviewed. And because only the latest published release is supported — there are no backports — staying current is the security-maintenance path, not an optional upgrade.

Overriding cli-version away from the Action's embedded default is a deliberate act with a consequence worth understanding: it couples you to the report-schema and exit-status contract of that specific CLI, and a mismatch surfaces as a hard "CLI exit status and redacted report disagree" failure rather than a quietly wrong result. That fail-loud behavior is itself a trust property — the fuller treatment lives in the results reference.

What this model does not give you

A security model is defined as much by the boundaries it declines to cover as by the guarantees it makes. Three are worth stating outright, because assuming otherwise creates real exposure.

It does not manage secrets that carry rotation, replication, or service ownership. Rather than contend with another controller for a secret, the tool refuses such secrets as conflicts and touches nothing. The reasoning behind refusing rather than best-effort adopting is in ownership and scope; the conditions that trigger each refusal are catalogued in the reconciliation reference.

It cannot hide the secret names or repository paths your Git history already exposes. Structural redaction keeps those out of this tool's logs and report, but the committed repository is its own disclosure surface, governed entirely by your repository policy — not by anything the tool can do after the fact.

And it is not an AWS authenticator. It consumes an identity you establish and never mints or scopes one for you, so the blast radius of a run is precisely the blast radius of the credentials you handed it. Designing that identity for least privilege is your responsibility, and the starting point is grant AWS access.

Common misconceptions

"Everything in a SOPS document is confidential"

Only the encrypted values are. Keys, structure, unencrypted leaves, SOPS metadata, and the file path are cleartext in Git and are only as private as your repository. This is the assumption most likely to leak something.

"An attested binary is a safe binary"

Attestation proves the artifact's origin and integrity, not its correctness or its runtime behavior, and not the supply chain of its dependencies. It tells you the binary is genuinely the project's; it does not tell you the project's code is flawless.

"Redaction means I still have to scrub the logs and report myself"

The default logs and the report are safe by construction — an allowlist, not a best-effort filter — so they can be archived or shared as-is. Enabling --show-resource-names is the one deliberate exception, and only you can decide whether a given log sink is trusted enough for it.

"Pinning the Action to @v1 is good enough"

A floating tag can be moved; a full commit SHA cannot. Pin to the SHA behind a release, and upgrade forward, since only the latest release is supported.