§ How-to · Operate
Store secrets in the sealed state
Keep a secret in the sealed state, hand it to a provider with pravo.#Secret, rotate it with pravo.#SecretVersion, and see who set each version.
On this page
A secret is a named value that the sealed state keeps for its readers: a database password, an API token, a private key. Each time you set it, it gains a version. A run reads it with pravo.#Secret and hands it to a provider, and the value reaches no record of the state, no plan and no log. The bucket holds the value sealed to the same readers as the records, and the transactions of the state say who set each version.
Everyone who can read the state can read every secret in it. A secret for other readers belongs in another state, with another list of readers.
Note
Every command below runs against tools/fakes3, the in-memory S3 server in pravo's source, on port 9000, and the three people are three directories on one machine. The page uses the tls provider, which pravo init downloads.
1. Seal a state with roles
Seal the state to a list of readers explains this step. Here it is in short: three people, each with an identity, listed in .pravo/access/readers.txt with a role. Ada is an admin, Dan a writer and Eve a reader. The roles decide who may set a secret, who may remove one and who may only read.
package vault
$settings: state: {
backend: "s3"
bucket: "northwind-state"
prefix: "prod"
region: "eu-west-1"
endpoint: "http://127.0.0.1:9000"
path_style: true
}
ada ssh-ed25519 <ssh-key> x25519:<key> role:admin
dan ssh-ed25519 <ssh-key> x25519:<key> role:writer
eve ssh-ed25519 <ssh-key> x25519:<key> role:reader
Seal the state, then lock the roles in the state, so that a reader cannot make themselves an admin by editing the file in their checkout.
The state is sealed to ada dan eve.
Commit the readers file. Writers read the list from git:
.pravo/access/readers.txt
No recovery recipient. If every reader loses their key, the state cannot be read.
Add one with: pravo protect rewrap --recovery 3/5
ada: admin
dan: writer
eve: reader
Stored the roles as version 1.
Added roles-genesis <sha256> to .pravo/access/readers.txt. Commit it: until it is in the repository the roles of the file decide.
Commit .pravo/access/readers.txt with the roles-genesis line. A state that holds secrets is in format 5, which earlier versions of pravo refuse to open. The first pravo secret set raises the format, and a state with no secret stays as it is.
2. Set a secret
A writer sets a secret. The value is never an argument, so that the shell history and the list of processes do not hold it. secret set reads it from standard input, from a file with --file, from an environment variable with --from-env, or on a terminal from a prompt that does not show it.
Set prod/db_password, version 1.
NAME VERSION SET BY
prod/db_password 1 <age> ago dan
A name is words of letters, digits, dots, dashes and underscores, with a slash between parts. list shows no value and needs no key. Setting a name again writes a new version and keeps the old ones.
Eve is a reader. She cannot set a secret:
Error: your identity is a reader in .pravo/access/readers.txt, and a writer is needed to change the state.
3. Read a secret
Every reader can read. get prints the bare value, so that a script can use it.
correct-horse-battery
The bucket holds the name, the number of the version and the time in the clear, and the value sealed. Anyone who can list the bucket sees that a secret exists and when it changed. Nobody without a key of a reader sees what it holds.
secret/prod%002Fdb_password@00000001.json
0
4. Hand a secret to a provider
A run reads a secret as pravo.#Secret. It is an ephemeral value, like pravo.#SecretEnv in Keep secrets out of the state: it may go into the configuration of a provider, into a write-only attribute and into another ephemeral resource, and nowhere that is recorded.
The tls provider signs a certificate with a private key that it receives in private_key_pem_wo, a write-only attribute. The provider never stores it, so it cannot see that the key changed. Its private_key_pem_wo_version is the number that tells it. pravo.#SecretVersion is that number: the version of the secret that the run reads, as an ordinary value.
package vault
import (
"pravo.dev/pravo"
tlsp "pravo.dev/providers/hashicorp/tls"
)
$tls: tlsp.#Provider
signing_key: pravo.#Secret & {name: "prod/signing_key"}
signing_rev: pravo.#SecretVersion & {name: "prod/signing_key"}
cert: $tls.#Resource.tls_self_signed_cert & {
private_key_pem_wo: signing_key.value
private_key_pem_wo_version: signing_rev.version
subject: [{common_name: "example.test"}]
validity_period_hours: 24
allowed_uses: ["digital_signature"]
}
The secret does not exist yet. A run stops before any provider starts, and says what to run.
Error: signing_key: the state has no secret named prod/signing_key. Set it with: pravo secret set prod/signing_key
Dan sets the key. Any PEM private key works; this block makes one with openssl and passes it on standard input, so no file holds it.
Set prod/signing_key, version 1.
Plan: 1 to add, 0 to change, 0 to destroy.
Saved the plan to change.plan. To apply exactly these actions, run:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]
Look for the key in the state, in the plan file and in the bucket:
private_key_pem_wo: null
private_key_pem_wo_version: 1
0
0
The record holds null for the write-only attribute and the number of the version. The plan file is sealed like the state, and the bucket holds the certificate and no key.
5. Break it: a secret in a recorded place
An output is recorded in the state, so a secret may not be an output.
package vault
import "pravo.dev/pravo"
shown: pravo.#Output & {value: signing_key.value}
Error: output shown references signing_key; outputs are recorded in state, so ephemeral values cannot be used in them
An ordinary attribute is refused the same way, and the check runs before any provider does. pravo.#SecretVersion may go anywhere, since a number is not secret.
6. Rotate a secret
Setting the name again is the rotation. The new version is the one that the next run reads, and the number of the version changes the attribute that tells the provider to take the new value.
Set prod/signing_key, version 2.
~ private_key_pem_wo_version: 1 -> 2 # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.
The certificate is replaced, and nothing else changes in the plan. Apply it.
Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <txn>]
A pravo.#Secret reads the latest version at every plan and apply. To hold a node at one version, write version: 1 in it. The nodes of a run that read the same secret without a version read the same one, so a version that is set while a run goes changes the next run and not this one.
A plan file and the apply of it are two runs, and the apply reads the secret again. When a new version was set in between and the specification reads its number, the provider's plan differs from the saved one: the apply stops with exit code 3, as for any stale plan, and you make the plan again.
7. See who set which version
secret history lists the versions of a secret, newest first, with when each was set, who set it and the note, which is sealed with the value and so is shown to readers only.
prod/signing_key
v2 <time> dan value
rotated after the audit
v1 <time> dan value
issued for the example
Every secret command is a transaction of the state, signed by the identity that ran it, as apply is. state log shows them next to the applies, and protect status counts the versions that a signed transaction of a writer accounts for.
<txn> secret applied <time>
pravo secret set prod/signing_key
signed by dan (verified)
set prod/signing_key, version 2
set secret:prod/signing_key v1 -> v2
--
<txn> secret applied <time>
pravo secret set prod/signing_key
signed by dan (verified)
set prod/signing_key, version 1
set secret:prod/signing_key v1
Roles on · 1 admin · 1 writer · 1 reader · pinned, version 1 in the state · you are an admin
Secrets 2 secrets · 3 versions · 3 bound to a transaction of a writer
A version that no signed transaction of a writer accounts for is refused when a command reads it, with exit code 7. That is what stops a reader, who holds the data key, from putting a value in the bucket that a writer never set.
8. Use a secret in a command
pravo secret exec runs a command with secrets in its environment. The values are not written to a file, only the command sees them, and the exit code is that of the command. Everything after -- belongs to the command.
PGPASSWORD has 21 characters
9. Remove a secret and forget its old values
A writer sets, and an admin removes. Dan cannot remove the password, and Ada can.
Error: your identity is a writer in .pravo/access/readers.txt, and an admin is needed to remove a secret.
Removed prod/db_password, version 2. Its values stay in the state until pravo secret purge prod/db_password --keep 0.
NAME VERSION SET BY
prod/db_password 2 <age> ago ada removed
prod/signing_key 2 <age> ago dan
A removal is a version of its own. The values stay in the state until an admin forgets them, so that the history shows what was there. A run that reads the removed secret stops before it changes anything.
package vault
import "pravo.dev/pravo"
db: pravo.#Secret & {name: "prod/db_password"}
Error: db: the secret prod/db_password was removed. Set it again with: pravo secret set prod/db_password
secret purge forgets the old values of a secret and keeps the newest ones. The versions stay in the history, without their values.
Forgot 1 value of prod/signing_key.
prod/signing_key
v2 <time> dan value
rotated after the audit
v1 <time> dan purged
Error: prod/signing_key version 1: the value of this version was purged
exit=1
A purge removes the values from the state. A bucket that keeps earlier versions of its objects keeps these too, and everyone who could read the secret has read it. A value that leaked needs a new one.
10. When a reader leaves
Removing a reader from the file stops future versions from being sealed to them. It cannot take back what they read, and every secret they could open is known to them. Step 10 of sealing the state shows the removal. For the secrets, four things follow it.
- Remove the line, commit the file and run
pravo protect rewrap. Versions written from now on are sealed to the readers who are left. - Replace each secret the person could read at its source, such as the database or the provider of the token, and set the new value with
pravo secret set NAME. - Forget the old values with
pravo secret purge NAME. - Run
pravo protect rewrap --allandpravo protect gc. The first seals what is left under the new key, and the second drops the group the person was in.
pravo secret history NAME shows the versions that existed while they were a reader.
11. See the secrets in the viewer
pravo serve has a Secrets section, opened with e. It lists each secret with its latest version, who set it and when, and which nodes of the specification read it. It shows no value: the server holds no key for the values, and it reads the part of the objects that is in the clear and the transactions that wrote them.
It also says what is wrong. A secret that the specification reads and the state does not hold, a version that no signed transaction of a writer accounts for and a state that is not sealed are flagged, worst first, each with the command that mends it to copy. A state with nothing to flag shows one line, all clear. The tab carries a number only when something needs action. The Secrets section lists the parts.
The nodes that read a secret are in the graph, in the first column with the environment variables. A node's inspector says that its value is read from the sealed state and is never recorded.
What next
- pravo secret lists every flag, the roles, the exit codes and the objects in the bucket.
- Secrets and ephemeral values says where each kind of value may go and where it is recorded.
- Signatures and roles says what the seal covers for secrets and what it leaves open.
- Roles says what a reader, a writer and an admin may do.