Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What is Sesam?

sesam is a tool to manage secrets in git.

When developing and deploying software it is often required to store and load several secrets like database passwords, certificates or other credentials. Those should be stored encrypted and only the users requiring them should have access to them.

sesam allows leveled access with multiple users to those encrypted secrets and gives you a simple interface to manage both users and secrets.

Note

The term user does not necessarily refer to a person. A user can also be a machine, like a server where sesam is installed.

You might think of a password manager now, which is not too far off. A password manager is usually targeted at managing an individual secrets, while a secret manager is focused on sharing some of those secrets with other users in a team and machines.

Features

  • High integration with git.
  • Declarative config as main interface.
  • Different access levels through user groups.
  • Secure - common crypto, minimal info leakage in rest.
  • Familiarity to git users.
  • Decentralized & offline ready.
  • Safe to use (hard to accidentally push unencrypted secrets)
  • Versioned - by wrapping git.
  • Scriptable via CLI interface.
  • Fast encryption and decryption.
  • Almost zero dependencies.
  • Support for rotation and exchange of secrets.

In short, sesam fits well the GitOps model of infrastructure.

Learning

How to use this manual:

Built 2026-05-02 10:48:01 • commit: 8f2c446

Installation

While this is in development, you can only clone the repo and run task to build the software.

Note

You need to install task for this.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Changelog

Versioning schema

We use this versioning schema:

$YEAR.$MONTH:$NR_COMMIT_ON_MAIN

Example:

2026.08:321

Stability info

We just figured that we don't like to decide what a 1.0 version is. It puts a lot of pressure on the developer, sometimes leading developers to never release a 1.0 version because they feel like there's always something that needs stabilizing.

Instead we will inform you on this page when we consider sesam to be ready for general usage. Before that everything might still change. We guarantee only stable interfaces when this is reached. We of course try to not change things once we know we have some users, but it might get necessary.

Versions

TOOD: Put changelog in here.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Roadmap

TODO: Link milestone plan

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Init

Prerequisites

sesam relies a lot on git for some functionality. You can either use an existing repository to manage your secrets in or you can create a whole new one. If you use an existing repository we recommend an empty sub-directory to manage your secret files in. The .sesam directory does not need to be on the same level as the .git folder.

Warning

Sesam relies on git history to be linear. You should therefore disable git push --force in your repository. Most git forges allow this in their settings (example: GitHub)

If force pushes are possible, someone could truncate the audit log (see Design for more info).

Creating a new repository

In the folder you've selected run:

$ sesam init --user bob --identity ~/.ssh/bobs_key --commit

Some things to note here:

  • This command will do the following:
    • Create a folder .sesam/ in the current directory.
    • Create a default config file in sesam.yml. It is a declarative config describing
    • Create a .gitignore that ignores everything but .sesam/ and .sesam.yml. This is to protect revealed secret so they never get accidentally added to git.
    • It will also create a first secret: README.sesam. Read it for a condensed version of this tutorial.
  • The --commit will add commit directly. Remove it if you don't want that.
  • You need to specify an initial user. This user will be the first admin. sesam has the concept of users with different access levels. As admin, bob has access to all secrets and can also create new users.
  • Every user needs an identity - a cryptographic way to prove he is this specific user. In the example above we used an ssh key.

Note

The --idenity option has to be passed to most sesam commands. Typing this out is tedious, but luckily we support specifying almost all command line flags as environment variable. If you place this in your .bashrc (or whatever you use), then you never need to specify the identity path again:

export SESAM_IDENTITY=~/.ssh/bobs_key

The rest of this guide assumes that you've exported an environment variable.

Identities

sesam supports the following keys as identity:

  • SSH Keys (RSA and ed25519)
  • Age Keys (generated by age-keygen)

If you want to use several of them you can also pass --identity (or short -i) several times. Then sesam will use all of them for encryption and decryption.

You are responsible for storing your identity in a safe place. You should not store it as part of the sesam repository.

Note

The list of possible identities will be likely supported in future releases with things like Yubikeys. Being based on age allows us to use their plugin system with relatively small effort.

If you want to know as what user you identify as, just type:

$ sesam id
bob

Recipients

Every user of sesam has at least one recipient. Think of it as the public part to the identity. While only you possess your identity, everyone has access to all recipients.

Built 2026-04-25 11:53:15 • commit: dfe0649

Managing secrets

Adding a secret via CLI

Note

All secrets must be in the same folder as sesam.yml or below it. We do not support adding secrets outside of the sesam repository.

If you have a secret at path/to/secret, then having it managed by sesam is only a matter of this command:

$ sesam add path/to/secret

This will:

  1. Record that this file is now managed by sesam by adding it to the audit log.
  2. Encrypt the file and place it in .sesam/objects. This is what is being pushed in the end.

If you also like to have it committed, then just append a --commit.

Adding a secret via Config

Adding secrets via CLI is nice for scripts. sesam also supports describing the desired state in a declarative way via sesam.yml. If you executed the above command you will notice the secret was added already to the config:

config:
  secrets:
    - path: path/to/secret
      description: Where it used, who owns it, Contact...

If you did not run the add command above, then you can also add the entry manually and then run:

$ sesam apply

This will automatically check what the state is in the repo and how it differs from the state in the config. The changes are then resolved by adding/removing secrets or adding/removing users.

Adding multiple secrets

You can also add whole directories, if you need to:

$ tree dir/of/secrets
.
├── some_file
└── sub
    └── another_file
$ sesam add dir/of/secrets
$ tree dir/of/secrets
.
├── sesam.yml
├── some_file
└── sub
    ├── another_file
    └── sesam.yml

This will create a config hierarchy of sesam.yml files in the config:

# Main sesam.yml:
config:
  secrets:
    - include: dir/of/secrets
# dir/of/secrets sesam.yml:
config:
  secrets:
    - include: sub
    - path: some_file
# sub sesam.yml:
config:
  secrets:
    - path: another_file

Once done you can also add descriptions to the files in the config or do more fine-tuning with the available config keys.

Note

If you ever create new files in the sub directories they do not automatically get added. Instead you need to run sesam add again. This will also remove secrets that are not there anymore, if any. In that sense, it works a bit like git add.

Modifying secrets

Running sesam add will work too though, adding them is idempotent. It is enough to run sesam seal if you only modified existing secrets though. This simply encrypts ("seals") all known secrets. As per usual, it also has a --commit option.

Removing secrets

If you have deleted files you can run this:

$ sesam add --deleted files/ dir/

Note

The command above only helps you deleted files on disk and want to tell sesam now that these files do not exist anymore. If you removed them from the config, then sesam apply will not find them anymore.

Listing secrets

$ sesam list secrets
├── README.sesam
└── dir
    └── of
        └── secrets
            ├── some_file
            └── sub
                └── another_file

You can also use the --json switch to print it in a more scriptable way.

Built 2026-04-25 11:53:25 • commit: 0f5d524

Managing users

Managing users via config

As mentioned during Initialization there is always at least one admin user. When you created your admin repo you will see something like this in your config:

config:
  users:
    - name: bob
      desc: Bob the Builder
      pub: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6VzKY/HxjYdIjBnRi6Nq7/0ydsKpX3uk1gu/ywUDJj
  groups:
    admin:
      - bob

As you can see, bob is an admin. Let's assume we are building a cloud backend in a team and want to give some users the access to the required secrets for deployment. We can do so by adding some more users and a new group:

   users:
     - name: bob
       desc: Bob the Builder
       pub: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN6VzKY/HxjYdIjBnRi6Nq7/0ydsKpX3uk1gu/ywUDJj
+    - name: alice
+      desc: Mrs. Wonderland
+      pub: github:alice
+    - name: peter
+      desc: Peter Lustig
+      pub: file://keys/peter.txt
   groups:
     admin:
       - bob
+    deployment:
+      - alice
+      - peter

We've used two new ways to fetch the keys:

  • github:alice will use all configured public keys of the GitHub user alice. Many forges support API to fetch this information. You can also use gitlab: or codeberg:. This makes adding new users really easy, as you most likely already know the user name of your peer on your favorite forge. The public key will be fetched only once initially and the result is cached. Apart from the first time there is no online access required therefore.
  • Peter on the other hand might not have an forge account. Maybe he also has an awful long RSA key that you don't want to put in the config verbatim. In this case you can just create a file in the repo and add it there.
  • The key of bob was deferred from the identity used during init. If you use the same public key for (e.g.) your GitHub account you can also write something like github:bob there.

Once we've changed the config we can this command, which should be familiar by now. This will then adjust the repository state accordingly:

$ sesam apply
- added user `alice`
- added user `peter`

Changing groups later works the same way.

Only admins may add/change other user and groups. If you're not an admin (determined by your identity) you will get an error.

Adding users and groups does not automatically give them access to secrets. We have to specify for each secret which groups have access to them (Reminder: the admin group has access always). Let's add them:

secrets:
  - path: some_password.txt
+   access:
+   - deployment

If you run sesam apply again, other users will have access. You have to commit (if you did not use --commit of course) and push it via git, of course. Then the others can pull the changes:

# on the laptop of alice:
$ sesam reveal --pull

Managing users via CLI

You can have the same effect without editing configs - which is nice for scripting:

# Add users like above:
$ sesam tell --user alice --desc "Mrs. Wonderland" --pub "github:alice"
$ sesam tell --user peter --desc "Peter Lustig" --pub "file://keys/peter.txt"
# --access can be given several times:
$ sesam add --path some_password.txt --access deployment

Files automatically get re-encrypted ("sealed").

Removing users

Removing users is also something only admins can do:

$ sesam kill alice

This will remove alice from all the access, delete any group that is now empty and then re-encrypt all files.

Note

You can not remove the last admin. There has to be always at least one user.

Built 2026-04-23 19:21:39 • commit: 55ddc0d

Verify

Since sesam is a tool focused on security. We need to constantly check whether we have been compromised and warn the user therefore. There are several checks that are being run, which will be explained in this chapter.

Warning

We recommend running sesam verify --all as part of your CI/CD pipeline.

Audit Log

The audit log is a list of entries, each describing a change to the repository. You can view it in .sesam/audit/log.jsonl. Each entry is linked to the previous one via a hash and protected by a signature of the user that made the change.

Additionally, we store the hash of the first entry and check if it was modified over git history as trust anchor. This makes truncating the log harder.

On almost every sesam command we will verify the integrity of the log. Failure to do so is fatal and requires investigation on why the state could not be verified.

Audit Log truncate

Check commit tree to see if the audit log in the commit before was a prefix of the current one. The log is completely linear, so this catches malicious truncation events.

This will be run on sesam verify --trunc

File integrity

The age encryption format does not by default provide integrity. One could still try to replace it with another file. Luckily, sesam writes a signature and hash for each file and thus allows catching deviations from the expected state.

This will be run on sesam verify --fsck

Automatically run on sesam reveal --pull.

Forge checks

When using forge user IDs like github:sahib we can check whether they match with the locally cached public key. This is not a security issue per se, but it can mean that a user might have locked himself out because he might have removed his old key.

This will be run on sesam verify --forge

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Tips & Tricks

YAML anchors

If you want to re-use a part of your configuration, you can create a snippet:

# Everything starting with "x-" on toplevel will be ignored.
x-default-access: &default-access
  access:
    - group1
    - group2
    - group3

# Use it: 
secrets:
  - path: foo.txt
    <<: *default-access
  - path: bar.txt
    <<: *default-access

For less often-used snippets it is sometimes useful to just reference another part directly:

secrets:
  - path: foo.txt
    access: &default-access
      - group1
      - group2
      - group3
  - path: bar.txt
    <<: *default-access

Read up on YAML anchors for more background.

Git integration

Unlike most other tools, sesam integrates more with git to show you diffs and record a consistent state on checkouts.

Diffing

On init, we've setup diff filters via the .gitattributes file. This means that git will pipe every change through sesam reveal before showing as diff.

git diff HEAD^ should therefore just work out of the box and show you locally what was changed.

Checkout

Something similar happens on checkout with smudge filters. When you check out an older state with git we automatically reveal a fitting state. Files you do not have access to are left out though.

Audit log

sesam is based on a log that keeps track of all modifications made in the repository. It can be useful to view it, if you're unsure on what happened:

$ sesam log

Config linting

This will check your config for validity and report any issue:

$ sesam lint
Built 2026-04-22 13:40:37 • commit: 3f26ffb

Template secrets

So far we did not really talk about how Secrets actually look like. We just assumed it was a file with a password in it or some x509 certificate. It is rather common though that secrets are embedded in a larger structure.

On the other hand, quite often we have files that contain several secrets. Let's assume we're building some service that is being fed environment variables from a file like this:

# SMTP variables:
export SMTP_USER=schorsch
export SMTP_PASSWORD="horsebatterystaple"

# Postgres variables:
export POSTGRES_USER=schorsch
export POSTGRES_PASSWORD="nevergonnagiveyouup"

# ...

Note

Just adding the whole file as secret is fine too. However, if you want to use features like Rotation then you need to split them up. Also, we believe splitting them up is a tidier since you can generate the output file via a template easily.

We can model such a case using template secrets:

  - type: template
    path: secrets.env
    access: [deploy]
    template: |
      | # SMTP variables:
      | export SMTP_USER=schorsch
      | export SMTP_PASSWORD="<<smtp_password>>"
      |
      | # Postgres variables:
      | export POSTGRES_USER=schorsch
      | export POSTGRES_PASSWORD="<<postgres_password>>"
    secrets:
      # The keys are the same as for regular secrets, except:
      # - `path` is optional. If you leave it out, the password string is only stored in the rendered template.
      # - Each secret needs a "name" that is used for replacement above.
      # - They don't need to be on disk. Each secret can be read back by using the placeholder.
      # - The special "encoding" allows using secrets with all kind of characters in env files, json, ...
      - type: password
        name: smtp_password
        encoding: shell # json, url, ...
      # You can also include other files in here if you want to.
      - include: other.yml
Built 2026-04-22 13:40:37 • commit: 3f26ffb

Rotation

From our experience, the biggest security threat are not holes in the software itself, but social factors. Colleagues leaving the company for example could still have a local copy of all secrets. While you will 99% of the time leave always on good terms you still have to consider those secrets as lost for the other 1%.

Note

We use those terms:

rotate: Replace a secret with a new secret of the same format. For example, an old password is replaced with a new one.

exchange: Replace a rotated secret at the place where it was used. For example, an ssh key that was rotated needs to be changed in authorized_keys.

In reality there is therefore no way to not rotate and exchange secrets from time to time. We gave sesam therefore features that help with automating this tedious process.

TODO: Write.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Branching

TODO: Explain how branching works with a linear audit log and how conflicts are handled and merging works.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

User identity exchange

Similar to regular secrets, you sometimes have to change the identities of certain sesam users. This document gives you guidance on how to do this safely.

TODO: write.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

Config Reference

TODO: Render a documentation here based on the json schema.

Built 2026-04-25 11:52:22 • commit: be4855d

CLI

TODO: Generate markdown from urfave/cli definitions and put it here as up-to-date reference.

Built 2026-04-21 08:33:04 • commit: 4adeb5b

Design

TODO: Those are rough remarks. Clean up later and draw some diagrams.

Architecture

  • We use age for hybrid encryption/decryption.
    • age supports its own key format as well common ssh keys.
    • It also supports Postquantum crypto already.
    • age's plugin system allows integration of Yubikeys and much more.
    • We don't roll our own crypto, which is always a good thing.
  • The handling of elaborate private key setups is to be done by the user to allow flexibility.
    • Exception: We support passphrase protected ssh keys as common use case.
  • Users have a public key they are referenced by.
    • Supported keys: age native (X25519) or SSH keys.
    • Users can also use forge-usernames (e.g. github:sahib)
    • All users know the public key of all other users through the config.
    • User are identified by private key ("identity").
    • Users are put into groups.
    • Only the pre-existing admin group may add new users/groups.
    • Every secret has a list of groups it may be accessed by.
    • Only users having access to a secret can change this access list.
  • Age keys support no signing, we therefore generate a ed25519 signing key for each user.
    • Keys are stored as .sesam/signkeys/$user.age.
  • All encrypted files and repository state are stored in a .sesam directory.
  • The sesam.yml file (see example in this repo) is declarative, i.e.
  • All operations that are changing the repository state are logged in an audit log.
    • All entries in the audit log are signed and reference the previous entry via hash.
    • This makes the log append-only and verifiable.
    • We also can re-construct the supposed state from the log.
    • This state could be also diffed to the existing sesam.yml to find diffs.
    • Diffs can be therefore detected in case of verify (i.e. malicious changes).
    • Diffs can also be applied in case of local changes before push (sesam apply)
    • Verification is run before any important operation.
OperationNeedsSource
Seal (encrypt)Recipients' age public keysRepo config
Reveal (decrypt)User's age identityLocal (key file, SSH key, plugin — user's choice)
SignEd25519 signing private keyDecrypt .sesam/signkeys/$user.age via age
VerifyEd25519 signing public keyRepo (plaintext)

Configuration

See sesam.yml for an annotated example file.

Rotation

We want it to make it possible to rotate and exchange existing secrets easily. Supported types would be for example:

  • Ssh key:
    • Generate: ssh-keygen (take settings over from existing?)
    • Exchange: ssh into server, add to authorized_keys, verify it works, remove old one, verify it still works and that old one does not work.
    • Config: Host, ssh-user, key-gen settings?
  • Password:
    • Generate: just a simple pwgen
    • Exchange: Hmm. Probably via a script?
    • Config: zxcvbn min score, alterantively length and other pwgen settings.
  • Template:
    • Meta secret type that allows generation inside an existing file.
    • Basically a "container" for one or several other secrets.
  • AWS/Github/[...] keys. Needs per-service integration if possible.
    • Integration should be optional and not baked into the main binary.
  • Custom
    • Generate: script
    • Exchange: script

The steps of a rotation would be:

  • plan: Show which secrets are rotated, which are exchanged.
  • exec: Execute the plan above.
  • todo: keep track of manual work that could not be automated with command to mark items done.

Other notes

  • We should have some git integration:
    • do an automatic git pull to check for changes
    • allow use of gitattributes to show local diffs between encrypted files. (smudge/clean filters)
    • Integrate as git command (git sesam)
    • Encourage using signed commits when pushing something with sesam
  • We should be able to reveal/seal whole directories where it makes sense.
  • Force pushes should be disabled for the repo and users should be made aware.
  • We should allow working in parallel where possible (e.g. encrypt only files that changed).
  • Implement command to view ownership of files easily.
  • Adding/Removing persons require re-encryption of all files.
  • sesam should support several .sesam dirs per git repo, .git and .sesam don't need to be in the same folder.
  • README: Make clear that this is not vibe coded. Also mention that we think about rewriting in Rust after 1.0
  • We should use multicode to encode hashes, priv/pub keys and signatures: https://github.com/sj14/multicode This way we can figure out if a byte blob is a signature, hash or something else.

Verify

Checks the integrity of the entire repository without revealing secrets. Implicitly called after pull, reveal or seal. Should also run in CI.

Audit log

Append-only, hash-chained log of all state-changing operations. Stored under .sesam/audit/log.json (chunking planned for later).

Entry structure:

FieldDescription
seq_idMonotonic sequence number (starting at 1)
prev_hashSHA3-256 (multihash-encoded) of the previous entry
operationOperation type (see below)
timeISO8601 UTC timestamp
changed_byUser that executed the operation
detailOperation-specific data (see below)
signatureEd25519 signature over all other fields (canonical JSON)

Operation types:

OperationDetail fieldsNotes
initInitUUID, Admin (embedded UserTell)Trust root. Pins first admin. See below.
user.tellUser, PubKeys, SignPubKeys, GroupsMust be signed by an admin.
user.killUserMust not remove last user or last admin.
secret.changeRevealedPath, GroupsAdd or update a secret and its access list.
secret.removeRevealedPathOnly users with access may remove.
sealRootHash, FilesSealedHash over all sorted .sig.json files.

Group membership is part of the user.tell detail. There are no separate group operations. Changing a user's groups means user.kill + user.tell. Admin status is determined by membership in the "admin" group.

Key rotation is also handled as user.kill + user.tell. The log doubles as key archive: past user.tell entries record which signing and encryption keys were valid at which point, so old signatures stay verifiable.

Authorization

Every entry that modifies users or secrets must be signed by an admin. The entry's signature field proves who wrote it. During verification we check that the signer was a member of the "admin" group at that point in the log.

The first admin is established by the init entry itself (embedded Admin field). There is no separate bootstrap user.tell.

Trust anchor (.sesam/audit/init)

sesam init writes the SHA3-256 hash of the init entry (seq 1) to .sesam/audit/init. This file is created once and must never change.

During verification, the hash of the current seq 1 entry is compared to this file. If they differ, the log was rebuilt from scratch. CI can additionally check that git log -- .sesam/audit/init has exactly one commit.

Does not protect against git push --force (Eve can rewrite the first commit and make everything consistent). Force push protection is outside sesam's threat model and should be enforced at the forge level.

Tamper detection

Three checks work together:

  1. Chain integrity: prev_hash of each entry must equal the SHA3-256 of the previous entry. Any modification, insertion or deletion breaks the chain.

  2. Trust anchor: The hash of the seq 1 entry must match .sesam/audit/init. If not, the entire log was replaced.

  3. State-vs-log consistency: Replaying the log must produce a model that matches the actual state on disk:

    • Users and their groups must match sesam.yml.
    • Secrets and their access lists must match sesam.yml.
    • The RootHash in the latest seal entry must match the hash computed from the .sig.json files on disk.

Attack scenarios and which check catches them:

  • Eve modifies the config but skips the log: state-vs-log fails (replayed model does not match sesam.yml).
  • Eve adds forged log entries: signature check fails (signer is not an admin).
  • Eve replaces the entire log: trust anchor check fails (init hash does not match .sesam/audit/init).
  • Eve replaces encrypted files: the RootHash in the seal entry no longer matches the .sig.json files.

Branching and merging

The audit log is linear and hash-chained. When two branches diverge, each appends its own entries with valid chains. On merge, git produces conflict markers in log.jsonl. Sesam detects and resolves these automatically — no separate merge tool is needed.

Conflict detection

LoadAuditLog detects git conflict markers (<<<<<<<) in the JSONL file. It parses both sides, finds the common prefix (entries shared before the fork), and produces two divergent tails: "ours" (HEAD) and "theirs" (incoming branch).

Replay strategy

The merge is linearized: "ours" entries are kept in place, "theirs" entries are replayed on top. Each replayed entry gets a new seq_id, prev_hash, and is re-signed by the merging user. The merging user does not need admin privileges — the original authorization is established by the changed_by field and git history (similar to how the init file's integrity relies on git history).

If replay fails (e.g. both branches added the same user, or a replayed entry references a user that was removed on "ours"), the merge is aborted with a diagnostic. The user must resolve the conflict on one branch first, then retry.

Secret content conflicts

Encrypted files (.age, .sig.json) are marked as binary in .gitattributes (set up by sesam init), so git does not produce conflict markers for them — it keeps "ours" and marks the path as conflicted.

If the same secret was sealed with different content on both branches, the replay renames the incoming version:

secrets/db_pass              ← ours (unchanged)
secrets/db_pass.theirs       ← theirs (renamed during replay)

Both are valid secrets in the audit log with their own access groups. The user inspects both, keeps the one they want, and removes the other via sesam rm. After cleanup, a sesam seal produces a consistent state.

.gitattributes

sesam init should generate:

.sesam/objects/**/*.age binary
.sesam/objects/**/*.sig.json binary

This prevents git from attempting text merges on encrypted content.

Additional checks

  • For each secret: .sig.json signature and ciphertext hash must be valid.
  • For each user: at least one public key must match the configured identity.
  • Freshly added secrets: warn if the adding user has no access to them.

Overview

┌─────────────────────────────────────────────────────────┐
│                     SecretManager                       │
│          ties everything together for a session         │
└────┬──────────┬───────────────┬─────────────────┬───────┘
     │          │               │                 │
     ▼          ▼               ▼                 ▼
┌────────┐ ┌────────┐    ┌──────────┐    ┌───────────────┐
│Identity│ │ Signer │    │ Keyring  │    │ VerifiedState │
│ your   │ │ signs  │    │everyone's│    │ "what should  │
│ private│ │ entries│    │public    │    │  the repo     │
│ key(s) │ │&secrets│    │keys      │    │  look like?"  │
└───┬────┘ └───┬────┘    └────┬─────┘    └───────┬───────┘
    │          │              ▲                   ▲
    │          │              │ keys added        │ built by
    │          │              │ during replay     │ replaying
    │          ▼              │                   │
    │       ┌─────────────────┴───────────────────┘─────┐
    │       │ AuditLog                                  │
    │       │  append-only, hash-chained, signed        │
    │       │  entries each entry is one of:            │
    │       │   - Init (+first admin)  - UserTell/Kill  │
    │       │   - SecretChange/Remove  - Seal           │
    │       └───────────────────────────────────────────┘
    │
    ▼
┌────────────────────────────────────────────────┐
│ Secret                                         │
│  Seal():   encrypt + sign ──► .age + .sig.json │
│  Reveal(): decrypt via identity                │
│  recipients come from VerifiedState + Keyring  │
└───────────────────────┬────────────────────────┘
                        │
                        ▼
              ┌───────────────────┐
              │ SecretSignature   │
              │  per-file hash    │
              └────────┬──────────┘
                       │
                       ▼
              BuildRootHash()
               combined hash of all .sig.json,
               stored in Seal entries

Verify():
 1. check .sesam/audit/init not tampered (git history)
 2. replay log: check chain, signatures, authorization
 3. compare resulting state against repo on disk
Built 2026-04-22 13:40:37 • commit: 3f26ffb

Security

TODO: Document the security model here.

Built 2026-04-22 13:40:37 • commit: 3f26ffb

AI-generated. This document was written by an AI assistant using training-data knowledge (cutoff Aug 2025). Verify specific claims before relying on them — project activity, feature sets, and security properties change.

Secret Management Alternatives

Why another tool?

  • We like to have a decentralized tool that works well together with git.
  • We need a tool that is easy to understand and reason about.
  • None of the above decentralized tools support leveled access.
  • Central tools are targeting really large organisations.

In general, our background is with git-secret. It was working kinda, but had way too much bugs, pitfalls, inconveniences and missing features to keep it for any longer.

Decentralized / Git-native tools

Overview

ToolLangSinceMaintenanceGit integrationEncryption
git-cryptC++2013slowtransparent (clean/smudge)AES-256-GCM
TranscryptBash2014activetransparent (clean/smudge)AES-256-CBC¹
git-secretBash2015activeexplicit hide/revealGPG (RSA/curve)
keyringerBash2012dormantexplicit encrypt/decryptGPG
BlackBoxBash2013dormantexplicit encrypt/decryptGPG
gopassGo2017activegit backend (pass-compatible)GPG / age
sopsGo2015very activenone native²age / PGP / KMS
ageboxGo2021moderatenone native²age (X25519)
Sealed SecretsGo2018activecommit sealed YAMLRSA-OAEP + AES-GCM
sesamGo2025in developmenttransparent (clean/smudge, planned) + pre-commitage / ChaCha20-Poly1305

¹ AES-CBC via OpenSSL — considered weaker than GCM/ChaCha20.
² Works alongside git but requires explicit encrypt/decrypt invocation.

Access control

ToolMulti-userDecl. configPer-file ACLLeveled access
git-cryptGPG or symmetric
Transcryptsymmetric (shared secret)
git-secretGPG keyring
keyringerGPG keyring
BlackBoxGPG keyring
gopassteam mounts
sopsyes
ageboxage recipients
Sealed Secretscluster RBAC✓ (cluster RBAC)
sesamage recipients

Security

ToolNo GPGSigned entriesAudit logKey rotationRekey on removal
git-cryptpoor (manual)
Transcryptpoor
git-secretmanual
keyringermanual
BlackBoxmanual
gopasspartialmanual
sopssops rotatemanual
ageboxpartialmanual
Sealed Secretsk8s auditkey renewal
sesam✓ (encrypted, signed, hash-chained)

Centralized / service-based tools

These require a running service or cloud dependency. Trade operational simplicity for availability risk.

ToolModelEncryptionAudit logLeveled accessDecl. configGit workflow
HashiCorp Vaultself-hosted serverAES-GCM (transit engine)✓ (detailed)✓ (policies + roles)✓ (HCL)env-inject or agent
InfisicalSaaS / self-hostedAES-256-GCM✓ (roles)env-inject, SDKs
DopplerSaaSAES-256✓ (roles)env-inject, CLI sync
1Password CLISaaS (op)AES-256-GCM✓ (vault permissions)partialenv-inject (op run), SDKs
AWS Secrets ManagerAWS managedAES-256 (KMS)✓ (CloudTrail)✓ (IAM policies)✓ (IaC/CDK)SDK / env-inject
GCP Secret ManagerGCP managedAES-256 (CMEK opt.)✓ (Cloud Audit)✓ (IAM roles)✓ (IaC/Terraform)SDK / env-inject
Ansible Vaultfile-based (no server)AES-256✓ (playbooks)committed ciphertext

When centralized tools win: large teams, compliance requirements (SOC2, HIPAA), dynamic secrets (database credentials), or when you need secret leasing / TTLs.
When git-native tools win: small teams, offline-first, no extra infrastructure, secrets version-controlled alongside code.


sesam vs. closest alternatives

git-cryptageboxsesam
Transparent git UX✓ (planned)
Modern crypto (no GPG)✗ (GPG mode)
Per-user access control
Declarative config
Leveled access (admin/user)
Signed + chained audit log
Rekeying on user removalmanual
Production-ready✗ (in development)
Built 2026-05-02 10:41:38 • commit: 2cce566

Developer Notes

Library

sesam can also be used as library.

TODO: Link go doc here

AI usage

  • Only mundane tasks
  • For security relevant things only as ideation partner
Built 2026-04-22 13:40:37 • commit: 3f26ffb