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.
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
gitusers. - 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:
- Go to Installation to grab your copy of
sesam. - Go to Basic Usage to walk through what it can do.
- Go to Advanced Usage if you need some more depth.
- Go to Reference if you need to look up things later on.
Installation
While this is in development, you can only clone the repo and run task to build the software.
You need to install task for this.
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.
Roadmap
TODO: Link milestone plan
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.
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
.gitignorethat 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.
- Create a folder
- The
--commitwill 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.
sesamhas the concept of users with different access levels. As admin,bobhas 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.
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.
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.
Managing secrets
Adding a secret via CLI
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:
- Record that this file is now managed by
sesamby adding it to the audit log. - 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.
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/
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.
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:alicewill use all configured public keys of the GitHub useralice. Many forges support API to fetch this information. You can also usegitlab:orcodeberg:. 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
bobwas 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 likegithub:bobthere.
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.
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.
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
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
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"
# ...
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
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%.
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.
Branching
TODO: Explain how branching works with a linear audit log and how conflicts are handled and merging works.
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.
Config Reference
TODO: Render a documentation here based on the json schema.
CLI
TODO: Generate markdown from urfave/cli definitions and put it here as up-to-date reference.
Design
TODO: Those are rough remarks. Clean up later and draw some diagrams.
Architecture
- We use age for hybrid encryption/decryption.
agesupports 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.
- Keys are stored as
- All encrypted files and repository state are stored in a
.sesamdirectory. - The
sesam.ymlfile (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.ymlto 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.
| Operation | Needs | Source |
|---|---|---|
| Seal (encrypt) | Recipients' age public keys | Repo config |
| Reveal (decrypt) | User's age identity | Local (key file, SSH key, plugin — user's choice) |
| Sign | Ed25519 signing private key | Decrypt .sesam/signkeys/$user.age via age |
| Verify | Ed25519 signing public key | Repo (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,
.gitand.sesamdon'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:
| Field | Description |
|---|---|
seq_id | Monotonic sequence number (starting at 1) |
prev_hash | SHA3-256 (multihash-encoded) of the previous entry |
operation | Operation type (see below) |
time | ISO8601 UTC timestamp |
changed_by | User that executed the operation |
detail | Operation-specific data (see below) |
signature | Ed25519 signature over all other fields (canonical JSON) |
Operation types:
| Operation | Detail fields | Notes |
|---|---|---|
init | InitUUID, Admin (embedded UserTell) | Trust root. Pins first admin. See below. |
user.tell | User, PubKeys, SignPubKeys, Groups | Must be signed by an admin. |
user.kill | User | Must not remove last user or last admin. |
secret.change | RevealedPath, Groups | Add or update a secret and its access list. |
secret.remove | RevealedPath | Only users with access may remove. |
seal | RootHash, FilesSealed | Hash 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:
-
Chain integrity:
prev_hashof each entry must equal the SHA3-256 of the previous entry. Any modification, insertion or deletion breaks the chain. -
Trust anchor: The hash of the seq 1 entry must match
.sesam/audit/init. If not, the entire log was replaced. -
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
RootHashin the latestsealentry must match the hash computed from the.sig.jsonfiles on disk.
- Users and their groups must match
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
RootHashin the seal entry no longer matches the.sig.jsonfiles.
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.jsonsignature 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
Security
TODO: Document the security model here.
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
| Tool | Lang | Since | Maintenance | Git integration | Encryption |
|---|---|---|---|---|---|
| git-crypt | C++ | 2013 | slow | transparent (clean/smudge) | AES-256-GCM |
| Transcrypt | Bash | 2014 | active | transparent (clean/smudge) | AES-256-CBC¹ |
| git-secret | Bash | 2015 | active | explicit hide/reveal | GPG (RSA/curve) |
| keyringer | Bash | 2012 | dormant | explicit encrypt/decrypt | GPG |
| BlackBox | Bash | 2013 | dormant | explicit encrypt/decrypt | GPG |
| gopass | Go | 2017 | active | git backend (pass-compatible) | GPG / age |
| sops | Go | 2015 | very active | none native² | age / PGP / KMS |
| agebox | Go | 2021 | moderate | none native² | age (X25519) |
| Sealed Secrets | Go | 2018 | active | commit sealed YAML | RSA-OAEP + AES-GCM |
| sesam | Go | 2025 | in development | transparent (clean/smudge, planned) + pre-commit | age / ChaCha20-Poly1305 |
¹ AES-CBC via OpenSSL — considered weaker than GCM/ChaCha20.
² Works alongside git but requires explicit encrypt/decrypt invocation.
Access control
| Tool | Multi-user | Decl. config | Per-file ACL | Leveled access |
|---|---|---|---|---|
| git-crypt | GPG or symmetric | ✗ | ✗ | ✗ |
| Transcrypt | symmetric (shared secret) | ✗ | ✗ | ✗ |
| git-secret | GPG keyring | ✗ | ✗ | ✗ |
| keyringer | GPG keyring | ✗ | ✗ | ✗ |
| BlackBox | GPG keyring | ✗ | ✗ | ✗ |
| gopass | team mounts | ✗ | ✗ | ✗ |
| sops | yes | ✓ | ✓ | ✗ |
| agebox | age recipients | ✓ | ✓ | ✗ |
| Sealed Secrets | cluster RBAC | ✓ | ✓ | ✓ (cluster RBAC) |
| sesam | age recipients | ✓ | ✓ | ✓ |
Security
| Tool | No GPG | Signed entries | Audit log | Key rotation | Rekey on removal |
|---|---|---|---|---|---|
| git-crypt | ✗ | ✗ | ✗ | poor (manual) | ✗ |
| Transcrypt | ✗ | ✗ | ✗ | poor | ✗ |
| git-secret | ✗ | ✗ | ✗ | manual | ✗ |
| keyringer | ✗ | ✗ | ✗ | manual | ✗ |
| BlackBox | ✗ | ✗ | ✗ | manual | ✗ |
| gopass | partial | ✗ | ✗ | manual | ✗ |
| sops | ✓ | ✗ | ✗ | sops rotate | manual |
| agebox | ✓ | ✗ | ✗ | partial | manual |
| Sealed Secrets | ✓ | ✗ | k8s audit | key renewal | ✗ |
| sesam | ✓ | ✓ | ✓ (encrypted, signed, hash-chained) | ✓ | ✓ |
Centralized / service-based tools
These require a running service or cloud dependency. Trade operational simplicity for availability risk.
| Tool | Model | Encryption | Audit log | Leveled access | Decl. config | Git workflow |
|---|---|---|---|---|---|---|
| HashiCorp Vault | self-hosted server | AES-GCM (transit engine) | ✓ (detailed) | ✓ (policies + roles) | ✓ (HCL) | env-inject or agent |
| Infisical | SaaS / self-hosted | AES-256-GCM | ✓ | ✓ (roles) | ✓ | env-inject, SDKs |
| Doppler | SaaS | AES-256 | ✓ | ✓ (roles) | ✓ | env-inject, CLI sync |
| 1Password CLI | SaaS (op) | AES-256-GCM | ✓ | ✓ (vault permissions) | partial | env-inject (op run), SDKs |
| AWS Secrets Manager | AWS managed | AES-256 (KMS) | ✓ (CloudTrail) | ✓ (IAM policies) | ✓ (IaC/CDK) | SDK / env-inject |
| GCP Secret Manager | GCP managed | AES-256 (CMEK opt.) | ✓ (Cloud Audit) | ✓ (IAM roles) | ✓ (IaC/Terraform) | SDK / env-inject |
| Ansible Vault | file-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-crypt | agebox | sesam | |
|---|---|---|---|
| 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 removal | ✗ | manual | ✓ |
| Production-ready | ✓ | ✓ | ✗ (in development) |
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