@konfig.ts/sops
@konfig.ts/sops is the secret backend for SOPS. You keep encrypted secret files in git, and at render time the package turns them into SopsSecret custom resources for the in-cluster sops-secrets-operator to reconcile. It offers two ways to get there and one helper:
| Export | What it does |
|---|---|
Sops.passthrough | Emits an already-encrypted SopsSecret file verbatim. Works offline; nothing is decrypted |
Sops.backend | Takes resolved plaintext values and re-encrypts them to the recipients you name |
Sops.source | Decrypts a SOPS file into a plaintext source you can feed to any backend, not only this one |
Install
bun add @konfig.ts/sopsTwo of the three need the sops CLI on the machine that runs konfig build. Sops.backend uses it to encrypt to the recipients you pass. Sops.source uses it to decrypt, with whatever key material sops finds for the file’s own sops: metadata. Sops.passthrough copies the file as-is and needs no CLI.
Usage
Passthrough mode: the full-stack example ships the GHCR pull secret as an encrypted SopsSecret file and provides it to other modules as Dep.Secret("ghcr-pull").
export const defineImagePulls = Application.module({
namespace: "app",
annotations: Sync.wave(-1),
provides: Dep.provideSecret("ghcr-pull"),
build: (ctx, opts: ImagePullsOpts) => {
const bound = Secret.bind({
secret: ghcrPull,
backend: Sops.passthrough({
file: `${opts.sopsBase}/SopsSecret-ghcr-pull.yaml`
})
})
return bound.manifest === undefined ? [] : [bound.manifest]
}
})Re-encrypting mode, for when the plaintext should flow through konfig. Here the values are decrypted from one SOPS file with Sops.source and re-encrypted to your recipients with Sops.backend:
import { dbCreds } from "@example/env-contracts"import { Secret } from "@konfig.ts/k8s"import { Sops } from "@konfig.ts/sops"
const bound = Secret.bind({ secret: dbCreds, backend: Sops.backend({ recipients: { age: ["age1..."] } }), source: Sops.source({ file: "infra/secrets/db-creds.enc.yaml", keys: ["url", "password"] })})Surface
| Export | Purpose |
|---|---|
Sops.passthrough | ({ file }): read and verify the file’s SopsSecret and emit it; requiresSource: false. Name and namespace are restamped to the bound contract only when the file was encrypted with mac_only_encrypted: true; otherwise a mismatch fails the render |
Sops.backend | ({ recipients, type? }): re-encrypt to age / kms / gcpKms / azureKv / pgp recipients; requiresSource: true |
Sops.source | ({ file, keys, extract? }): a SecretSource that decrypts once and yields Redacted values; extract(key, parsed) defaults to a flat { key: value } lookup |
sopsDecrypt({ file }), sopsEncryptStdin({ plaintextYaml, recipients }) | Low-level CLI wrappers (SopsDecryptInput, SopsEncryptStdinInput) |
SopsBackendOptions, SopsSourceInput, SopsRecipients, SopsSecret, SopsSecretSpec, SopsSecretTemplate | Option and CR types |
Errors
| Error | When it is raised |
|---|---|
SopsInvocationError | The sops process failed or was not found. Carries op ("decrypt" or "encrypt") and the cause. How you see it depends on where it happened: inside Sops.backend and Sops.passthrough it is wrapped in a RenderError; inside Sops.source it is wrapped in a SecretSourceError |
RenderError (cause: SopsUnencryptedValueError) | A value about to be emitted was still plaintext. Every emitted value is checked for the ENC[ marker (honouring sops.encrypted_regex); rather than emit plaintext dressed as a secret, the render fails with a RenderError whose cause is the internal SopsUnencryptedValueError |
Requirements
effect@^4.0.0-rc.111as a peer dependency (Effect 4, release-candidate line).- Depends on
@konfig.ts/core,@konfig.ts/env,@konfig.ts/k8s, andyaml. - Runtime: Bun recommended; Node >= 23.6 works; Node 22.6 to 23.5 with
--experimental-strip-types;tsxworks.
Source and README: packages/sops.