Skip to content
Docs menu / sops

@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:

ExportWhat it does
Sops.passthroughEmits an already-encrypted SopsSecret file verbatim. Works offline; nothing is decrypted
Sops.backendTakes resolved plaintext values and re-encrypts them to the recipients you name
Sops.sourceDecrypts a SOPS file into a plaintext source you can feed to any backend, not only this one

Install

Terminal window
bun add @konfig.ts/sops

Two 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").

infra/modules/image-pulls.ts
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:

infra/modules/db.ts
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

ExportPurpose
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, SopsSecretTemplateOption and CR types

Errors

ErrorWhen it is raised
SopsInvocationErrorThe 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.111 as a peer dependency (Effect 4, release-candidate line).
  • Depends on @konfig.ts/core, @konfig.ts/env, @konfig.ts/k8s, and yaml.
  • Runtime: Bun recommended; Node >= 23.6 works; Node 22.6 to 23.5 with --experimental-strip-types; tsx works.

Source and README: packages/sops.