Auto-Unsealing Vault Without Cloud KMS or a TPM, Using Tang and Clevis
Binding unseal keys to a network presence check instead of typing them in at every boot.
The problem
Vault starts sealed. Nothing can read a secret from it, including the services that are supposed to start using it, until someone supplies enough Shamir key shares to reconstruct the master key. That is exactly the property that makes Vault trustworthy at rest, and exactly the property that turns every reboot, every node replacement, and every planned maintenance window into a manual step someone has to be awake for.
Vault’s built-in auto-unseal mechanisms solve this by delegating the unwrap step to an external key-management service — AWS KMS, Azure Key Vault, GCP Cloud KMS, an HSM via PKCS#11, or a TPM. All of them assume infrastructure that a given environment may not have: no cloud provider, no HSM budget, and a TPM binds the secret to one specific piece of physical hardware, which is the wrong shape for a cluster where a Vault node might be rebuilt on different hardware entirely.
Network-bound disk encryption — Tang and Clevis, the mechanism most commonly used to
auto-unlock LUKS volumes at boot — solves a structurally similar problem for disk
encryption: bind a key to “can this host reach a specific server on the network right
now” rather than to a piece of hardware or a cloud account. Vault has no native Tang
integration, but nothing requires the key being unlocked to be a disk key. The same
primitive — encrypt a secret such that decrypting it requires reaching a Tang server —
works just as well applied directly to the Shamir unseal key shares, with a small script
supplying the decrypted result to vault operator unseal at boot instead of to cryptsetup.
Working through it
What Tang and Clevis actually provide
Tang is deliberately minimal: it holds a keypair and answers a key-derivation request over HTTP, and it never sees or stores the data being protected. Clevis, the client, encrypts data such that recovering it requires an exchange with a specific Tang server — but Tang’s answer alone is not the plaintext, so a compromised Tang server cannot decrypt data it never had, and a network capture of the exchange (McCallum-Relyea, the underlying key exchange) does not reveal the key either. What Tang provides is not confidentiality of the ciphertext — Clevis’s own encryption does that — it is a presence check: decryption succeeds only when the exchange with Tang succeeds, which in practice means “this host is on the network the Tang server is reachable from”.
This is the honest trade being made: an attacker who has already exfiltrated the encrypted key shares still needs network access to the Tang server to decrypt them, but anyone who can reach the Tang server and has the ciphertext can decrypt it too — there is no additional secret involved beyond reachability. It is a real barrier against a stolen disk or a stolen backup, not a substitute for access control on the host itself.
Binding a key share is no different from binding a disk key
echo -n "the-secret" | clevis encrypt tang '{"url":"http://tang.example.internal:8080"}' -y > secret.jwe
clevis decrypt < secret.jwe
clevis encrypt tang fetches the Tang server’s advertisement (its public keys) on first
use — the -y flag skips the interactive trust prompt, appropriate for a scripted setup
where the advertisement’s fingerprint has already been verified out of band. What comes out
the other end, secret.jwe, is a JSON Web Encryption object: ordinary ciphertext that is
safe to store on the disk it is meant to unseal.
Removing the single point of failure
One Tang server means one thing that has to be reachable at every boot, forever, which
just relocates the availability problem rather than solving it. Clevis’s sss (Shamir
Secret Sharing) pin can combine several Tang servers with its own threshold, independent of
Vault’s own Shamir threshold on the unseal keys themselves:
clevis encrypt sss '{"t":1,"pins":{"tang":[
{"url":"http://tang-a.example.internal:8080"},
{"url":"http://tang-b.example.internal:8080"}
]}}' -y < key-share.txt > key-share.jwe
"t":1 means any one of the two Tang servers being reachable is enough to decrypt —
favouring availability, at the cost that compromising either single server is also enough.
"t":2 would require both, favouring confidentiality of the presence check at the cost of
availability if either one is down at boot. This is tested and real, not theoretical: with
t:2, stopping either Tang server causes decryption to fail outright until it is back;
with t:1, either one alone is sufficient. Choose deliberately, and match it to how many
Tang servers are actually run and where.
Wiring it into Vault’s boot sequence
Vault has no hook that calls Clevis directly, so the integration is a small systemd unit
that runs before Vault starts serving, decrypts each key share, and calls
vault operator unseal once per share (Vault’s own Shamir threshold, separate from any
Clevis threshold, still applies — reaching the unseal endpoint enough times to satisfy
Vault’s own threshold value is still required).
The solution
Setting up the key material once, after vault operator init:
#!/usr/bin/env bash
# bind_unseal_keys.sh — run once, after `vault operator init`, on a
# machine that can reach the Tang servers and holds the raw key shares.
set -euo pipefail
TANG_PINS='{"t":1,"pins":{"tang":[
{"url":"http://tang-a.example.internal:8080"},
{"url":"http://tang-b.example.internal:8080"}
]}}'
install -d -m 700 /etc/vault-unseal
i=0
for key_share in "$@"; do
echo -n "$key_share" | clevis encrypt sss "$TANG_PINS" -y \
> "/etc/vault-unseal/key-${i}.jwe"
i=$((i + 1))
done
./bind_unseal_keys.sh "$UNSEAL_KEY_1" "$UNSEAL_KEY_2" "$UNSEAL_KEY_3"
The raw key shares passed as arguments exist only for the duration of this one-time setup run; they are not stored anywhere by this script, only their Clevis-encrypted form is.
The boot-time unseal script and its systemd unit:
#!/usr/bin/env bash
# /usr/local/sbin/vault-auto-unseal
set -euo pipefail
export VAULT_ADDR="https://127.0.0.1:8200"
for jwe in /etc/vault-unseal/key-*.jwe; do
key=$(clevis decrypt < "$jwe")
vault operator unseal "$key" >/dev/null
done
vault status
# /etc/systemd/system/vault-auto-unseal.service
[Unit]
Description=Unseal Vault using Clevis-bound key shares
After=vault.service network-online.target
Wants=network-online.target
Requires=vault.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/vault-auto-unseal
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now vault-auto-unseal.service
Verifying the failure mode is part of verifying the setup, not optional: stop every Tang
server the t threshold needs and confirm the unit fails loudly rather than starting Vault
in a half-configured state —
systemctl stop tangd.socket # on the tang hosts
systemctl restart vault-auto-unseal.service
journalctl -u vault-auto-unseal.service --no-pager | tail -5
should show clevis decrypt failing with a communication error and the unit reporting
failed, not a silent skip — and restarting the Tang servers and re-running the unit
should then unseal normally.
Conclusion
Tang and Clevis do not add a secret to the system, they add a reachability requirement to an already-encrypted secret — understanding that distinction is the difference between treating this as a real access control and over-trusting a network boundary as if it were one.
The same mechanism that autoseals a LUKS volume works unmodified on arbitrary key material,
because Clevis was never specific to disk encryption — it encrypts bytes, and Vault’s
unseal keys are just bytes with a particular meaning attached by vault operator unseal.
Running more than one Tang server and choosing the sss threshold deliberately is the
actual design decision here, not a footnote: t:1 trades confidentiality margin for boot
reliability, t:2 trades the reverse, and the right answer depends on how much the
organisation trusts its own network segmentation versus how much it can tolerate a boot
that waits on two servers instead of one.