sopsy 1.0.1

The missing developer experience for SOPS
Documentation
# Sopsy — The Missing Developer Encryptor

## Secrets Management Guide (Engineering Manager / Admin)

This guide is for the person who **owns** a sopsy-managed repository: you
bootstrap it, onboard and offboard developers, and keep the break-glass key
safe. For the day-to-day developer perspective, see the
[Developer guide](guide-developer.md); for the full command reference, the
[README](../README.md).

> [!NOTE]
> sopsy does not replace SOPS — it makes SOPS delightful. Everything below is
> ultimately `sops` + `age`; sopsy adds safe defaults, a CI gate, and the
> recipient bookkeeping that humans get wrong.

### Overview

This repository stores shared development secrets in Git in **encrypted form** using:

- SOPS
- age encryption

No plaintext secrets should ever be committed.

Only developers possessing an approved age private key can decrypt repository secrets.

> [!IMPORTANT]
> Plaintext secrets must **never** be committed. `sopsy init` configures
> `.gitignore` to keep them out, and `sopsy check` fails CI if one slips in — but
> as the admin you own the policy. Treat any leaked plaintext as a rotation event.

---

## Security Model

Each developer owns an individual key pair.

On macOS it is generated by the [Secure Enclave](https://support.apple.com/guide/security/the-secure-enclave-sec59b0b31ff/web), which subsequently holds the private key, and makes it entirely inaccessible to anyone or anything.

```txt
Developer

Private Key  → stays on developer laptop's Enclave
Public Key   → shared with repository maintainers
```

Only public keys are checked into the repository.

```mermaid
flowchart TB
    subgraph Devs["Developers (each on their own Mac)"]
        a["alice<br/>🔒 Enclave private key"]
        b["bob<br/>🔒 Enclave private key"]
    end
    subgraph BG["Offline vault (admins only)"]
        g["break-glass<br/>🔒 software age key"]
    end
    subgraph Repo["Git repository"]
        sy[".sops.yaml<br/>age: [alice, bob, break-glass]"]
        cfg[".sopsy.yml<br/>names + break-glass flag"]
        enc[".env.encrypted · *.encrypted"]
    end
    a -- public key --> sy
    b -- public key --> sy
    g -- public key --> sy
    sy -. recipients .-> enc
    sy --- cfg
```

The repository contains:

```txt
.sops.yaml
.sopsy.yml
.env.example
.env.encrypted
config/*.encrypted.yaml
```

The repository never contains:

```txt
.env
.env.production
*.pem
*.key
AWS credentials
API keys
```

> [!NOTE]
> sopsy keeps **two** config files in sync. `.sops.yaml` is consumed by `sops`
> itself (the `creation_rules` / `age` recipient lists). `.sopsy.yml` is sopsy's
> own richer metadata: human-readable recipient names, the break-glass marker,
> the encrypted-file globs, and the `sops` version. The `sopsy recipient`
> commands update both for you.

---

## Bootstrapping the repository

As the admin you run `sopsy init` **once** to set everything up:

```bash
cd my-repo
sopsy init                       # generates a Secure Enclave identity for you
# or, in CI / on non-Enclave hardware:
sopsy init -y --recipient-name ci --public-key age1… --no-generate
```

This writes `.sops.yaml`, `.env.example`, an encrypted `.env.encrypted`, the
`.gitignore` safety rules, and `.sopsy.yml`, then prints your public recipient.

> [!TIP]
> `init` is idempotent — re-running it preserves existing files unless you pass
> `--force`. Run `sopsy doctor` afterward to confirm the toolchain and repo are
> healthy, and set up the break-glass key right away (see below).

---

## Initial Setup (per developer)

Each developer generates their own key pair. On macOS Apple Silicon, prefer a
Secure Enclave identity:

```bash
age-plugin-se keygen --access-control=any-biometry-or-passcode -o ~/sopsy-identity.txt
# public key: age1se1xxxxxxxxxxxxxxxxxxxxxxxxxx
```

A software key also works (no hardware protection):

```bash
age-keygen -o ~/.config/sops/age/keys.txt
# public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

They send only their **public key**.

---

## The recipient lifecycle

Every membership change follows the same shape: edit the recipient set, then
`sops updatekeys` re-wraps every encrypted file's data key for the new set.
`sopsy recipient add` / `remove` do both steps for you.

```mermaid
flowchart LR
    add["sopsy recipient add NAME<br/>--public-key age1…"] --> mut1["update .sopsy.yml<br/>+ .sops.yaml"]
    rem["sopsy recipient remove NAME"] --> mut2["update .sopsy.yml<br/>+ .sops.yaml"]
    mut1 --> upd
    mut2 --> upd["sops updatekeys (per file)<br/>re-wrap data keys"]
    upd --> commit["commit re-encrypted files"]
    commit --> verify["sopsy check"]
```

> [!WARNING]
> Use `--no-updatekeys` only when you intend to batch several changes and run the
> re-encryption yourself afterward. Until `sops updatekeys` runs, a newly added
> recipient **cannot decrypt** existing secrets, and a removed recipient's key is
> still embedded in the ciphertext.

---

## Onboarding a Developer

```mermaid
sequenceDiagram
    actor Dev as New developer
    actor Admin
    participant Repo as repo (.sops.yaml/.sopsy.yml)
    Dev->>Admin: send public key age1se1…
    Admin->>Repo: sopsy recipient add alice --public-key age1se1…
    Note over Admin,Repo: re-encrypts every secret for the new recipient set
    Admin->>Repo: git commit + push
    Admin->>Dev: "added — git pull"
    Dev->>Repo: git pull && sopsy edit .env.encrypted ✅
```

1. Obtain the developer's **public key**.
2. Add them (updates both config files and re-encrypts):

   ```bash
   sopsy recipient add alice --public-key age1se1...
   ```

3. Commit the result and tell them to `git pull`.

The equivalent manual edit to `.sops.yaml` is shown below for reference, but
prefer the `sopsy recipient` command so `.sopsy.yml` stays in sync:

```yaml
creation_rules:
  - path_regex: '\.encrypted$'
    age:
      - age1alice...
      - age1bob...
      - age1charlie...
```

```bash
# Manual re-encryption (what sopsy runs for you, per file):
sops updatekeys -y --input-type dotenv .env.encrypted
```

Commit the result.

> [!TIP]
> `sopsy recipient list` prints the current roster (names, truncated keys, and
> the ★ break-glass marker) — a quick way to confirm an add or audit access.

---

## Offboarding a Developer

```bash
sopsy recipient remove alice
```

This removes alice from both config files and re-encrypts so her key is no
longer a recipient of **future** ciphertext.

> [!CAUTION]
> Removing a recipient does **not** retroactively protect secrets she already
> cloned — she still holds old ciphertext and her key decrypted it. When someone
> leaves with access to sensitive values, **rotate the underlying secrets**
> (database passwords, API keys) in addition to removing the recipient.
>
> sopsy also refuses to remove the **last remaining recipient** or the **sole
> break-glass recipient**, to avoid stranding the repository.

---

## Break-glass: disaster recovery

A break-glass key is a separate emergency `age` key pair, stored **offline**
(e.g. 1Password) and known to only a few admins. It is the recovery path if
every developer's Secure Enclave device is lost.

```mermaid
flowchart TD
    gen["age-keygen -o break-glass.key<br/>(offline)"] --> vault["store private key in a vault<br/>1Password · admins only"]
    gen --> reg["sopsy recipient add break-glass<br/>--public-key age1q… --break-glass"]
    reg --> ok["sopsy check ✔ break-glass present"]
    lost["💥 all Enclave devices lost"] --> recover["retrieve break-glass key from vault"]
    recover --> rekey["sops updatekeys / re-add new dev keys"]
```

```bash
# 1. Generate the emergency pair OFFLINE:
age-keygen -o break-glass.key          # prints: public key: age1q...
# 2. Store break-glass.key in the vault; register the PUBLIC key:
sopsy recipient add break-glass --public-key age1q... --break-glass
```

> [!CAUTION]
> Without a break-glass key, losing the Secure Enclave devices that hold the only
> recipient keys means **permanent, unrecoverable loss** of every secret. Set up
> break-glass on day one. Both `sopsy doctor` and `sopsy check` warn/fail until a
> break-glass recipient exists.

> [!IMPORTANT]
> The break-glass *private* key must live offline in a vault, never in the
> repository and never on a developer's daily-driver machine. Only its public
> key (`age1q…`) is committed — exactly like any other recipient.

---

## Enforcing hygiene in CI

Run `sopsy check` in CI and as a pre-commit hook. It validates seven invariants
(plaintext not tracked, `.env` ignored, `.sops.yaml` valid, every encrypted file
covered and genuinely encrypted, no plaintext secrets tracked, break-glass
present) and exits non-zero on any failure — **without needing any private key**.
See the [CI gate diagram and YAML example in the README](../README.md#using-sopsy-in-ci).

> [!TIP]
> Because `check` never decrypts, it runs on a Linux CI runner just fine. The
> Secure Enclave is only needed to *create* keys and *edit* secrets, not to
> verify hygiene.