پنهان · Persian for hidden

Secrets in Git.
Encrypted.

Penhan is a small CLI for teams that want Git to be the source of truth for their secrets. They live in the repository as encrypted files, reviewed and versioned like code. Penhan decrypts them on your machine and pushes what changed to HashiCorp Vault or Kubernetes.

v0.6.0 · Go · macOS and Linux · MIT


            

The real file: a 50-byte AES-256-GCM ciphertext, shown with xxd. The key stays in .penhan/keys/, which is gitignored.

§ 1 · How it works

Encrypt. Commit. Push what changed.

  1. 01

    Encrypt

    Write a flat YAML or JSON map, run penhan encrypt. Plaintext and keys are gitignored for you; only .enc files get committed.

    $ penhan encrypt
      Encrypted: secrets/db.yaml
  2. 02

    Commit

    Review secrets in pull requests like any other change. Re-encrypting an unchanged secret leaves its file alone, so diffs stay clean.

    $ git status --short -uall
    ?? penhan.yaml
    ?? secrets/db.yaml.enc
  3. 03

    Push

    check compares each secret with the backend; push writes only the new and changed ones.

    $ penhan push
      Pushed (new): db
    
    Push complete: 1 pushed, 0 unchanged
~/project

        

Real output from penhan v0.6 against a throwaway Vault dev server.

§ 2 · Backends

One repository, many safes.

A project holds one or more safes. Each is a directory with its own penhan.yaml, key and backend, so one repository can manage secrets for several apps, environments or namespaces.

  • HashiCorp Vault KV v2, with a token file per safe.
  • Kubernetes Secrets in a namespace, pinned to one kubeconfig context.
  • An encrypted directory, for anything else.
Encrypted files in Git and a local key go into penhan, which writes to HashiCorp Vault, Kubernetes Secrets or an encrypted directory Git repository secrets/*.yaml.enc encrypted, committed .penhan/keys/ local key, gitignored penhan check / push Vault KV v2 Kubernetes Secrets Directory encrypted files

§ 3 · Guardrails

Safe by default, because it's your secrets.

Pins the context
A Kubernetes safe is tied to one kubeconfig context, so a push can't land in the wrong cluster.
Never takes over
It won't overwrite a Secret it didn't create. penhan import adopts existing ones without changing them.
Never regenerates a key
A missing key is an error, not a reason to make a new one and orphan every file.
Byte for byte
Secret values reach the backend exactly as written: no trimming, no re-encoding.
Changes only
Hashes decide what's new or changed; unchanged secrets are skipped and reported as such.
Nothing to learn from history
AES-256-GCM or OpenPGP, keyed per safe and randomized on every encryption, so Git history never shows when two secrets match or a value changes back.

§ 4 · Configuration

One small file per safe.

penhan add writes it for you, with flags or interactively. This is the one from the session above.

Every option · Every command

# myapp/penhan.yaml
encryption:
    method: aes
    aes:
        key_path: .penhan/keys/aes.key
backend:
    type: vault
    vault:
        addr: http://127.0.0.1:8200
        token_path: .penhan/vault-token
        mount_path: secret
        base_path: myapp
secrets:
    path: secrets/
    format: yaml

§ 5 · Install

Prebuilt for macOS and Linux.

OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/miladbeigi/penhan/releases/latest | sed 's|.*/v||')
curl -fsSL "https://github.com/miladbeigi/penhan/releases/download/v${VERSION}/penhan_${VERSION}_${OS}_${ARCH}.tar.gz" | tar -xz penhan
sudo install penhan /usr/local/bin/

Update any time with penhan update, which installs the latest release after checking its checksum. Then run penhan add for an interactive setup, or follow Getting started.