§ Concepts

Secrets and ephemeral values

Which values pravo records in state, saved plans and outputs, what an ephemeral value is, where it may go, and what hiding a value does and does not do.

Time
14 min

View as Markdown

On this page

pravo handles three kinds of value that need care. A provider marks some attributes sensitive. Some values must never be recorded, and pravo calls them ephemeral. Some values come from the environment. Each travels through plans, the state and outputs in a different way. This page says where each one goes.

Where a value is recorded

Value State Saved plan Printed plan Plan JSON and policy document
An attribute of a resource or data source Recorded Recorded Printed Present
An attribute that the provider marks sensitive Recorded as returned Recorded (sensitive) Present, not masked
A value read with pravo.#Env Wherever it is used Wherever it is used Where it is used Where it is used
An output Recorded Recorded when it is known Printed Present
An output with sensitive: true Recorded, wrapped as $sensitive Recorded when it is known (sensitive) Present, not masked
An ephemeral value Never Never Never Never

Hiding a value is a matter of display. (sensitive) keeps a value out of a terminal, a log and a review. It does not keep the value out of the state, the saved plan or the plan document. An ephemeral value is the one kind that pravo keeps out of all of them.

Ephemeral values

An ephemeral resource is a provider object that exists for the length of one run. It is built through a provider like any other resource, from its #Ephemeral.TYPE definition. pravo opens it when the first resource that needs it runs. It renews it on the schedule that the provider asks for. It closes every open one when the run ends, whether the run succeeded or failed.

Plan and apply are separate runs. An ephemeral resource that a plan opened is closed at the end of the plan, and the apply opens it again. Nothing about it is written to the state or to a saved plan. The result lives in the running process. The Graph: line counts how many were opened.

Work in an empty directory. The specification below runs a command and uses its output as the password of a provider.

main.cue
package estate

import (
	"pravo.dev/pravo"
	loc "pravo.dev/providers/hashicorp/local"
	tlsp "pravo.dev/providers/hashicorp/tls"
)

$local: loc.#Provider

// An ephemeral resource: opened for the run, never recorded.
token: $local.#Ephemeral.local_command & {
	command:   "printf"
	arguments: ["hunter2-marker"]
}

// Provider configuration may use an ephemeral value.
$tls: tlsp.#Provider & {
	proxy: [{
		url:      "http://127.0.0.1:1"
		username: "pravo"
		password: token.stdout
	}]
}

deploy: $tls.#Resource.tls_private_key & {algorithm: "ED25519"}

public: pravo.#Output & {value: deploy.public_key_openssh}
private: pravo.#Output & {value: deploy.private_key_pem, sensitive: true}
terminal
$pravo init | tail -1
pravo has been initialised. Run "pravo plan" to see what it would do.
terminal
$pravo plan -out p.plan | grep -E '^(Plan|Graph)'
Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 2, 1 ephemeral opened.

The plan opened token. The apply of the saved plan opens it again.

terminal
$pravo apply p.plan | grep -E '^(Apply|token)'
token: Opening...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]

The marker hunter2-marker is nowhere in the state or in the saved plan.

terminal
$grep -r hunter2-marker pravo.state p.plan || echo "not found"
not found

Where an ephemeral value may go

Everything that a managed resource or a data source returns is recorded in the state, and so is every output. An ephemeral value may therefore be used in only three places.

  • The configuration of a provider, which is the fields of a provider struct.
  • A write-only attribute of a managed resource. The provider accepts it and never stores it.
  • Another ephemeral resource.
ephemeral value ephemeral resource pravo.#SecretEnv provider configuration accepted: fields of a provider struct write-only attribute accepted: the provider never stores it another ephemeral resource accepted: also lives for one run ordinary attribute error: recorded in the state output error: recorded in the state provider function argument error: a result can be recorded
fig. 1 Where an ephemeral value may go. Every refused place leads to the state.

A reference from any other attribute is an error that names the attribute and the ephemeral resource.

leak.cuerejected
package estate

leak: $local.#Resource.local_file & {
	filename: "leak.txt"
	content:  token.stdout
}

pravo checks the rule when it converts the body of a resource, before it asks the provider about that resource. pravo validate does not run the check.

terminal
$pravo validate
Success! 2 resources, 0 data sources, 1 ephemeral, 0 function calls, 2 outputs across 2 providers.
terminalexit 1
$pravo plan
token: Opening...

Error: leak.content: references token, but ephemeral values may only be used in write-only attributes, provider configuration and other ephemeral resources (anything else is recorded in state)
terminal
$rm leak.cue

An output is recorded in the state, so an ephemeral value cannot be an output. pravo checks the outputs before it plans.

leakout.cuerejected
package estate

import "pravo.dev/pravo"

leaked: pravo.#Output & {value: token.stdout}
terminalexit 1
$pravo plan
Error: output leaked references token; outputs are recorded in state, so ephemeral values cannot be used in them
terminal
$rm leakout.cue

A provider function does not accept an ephemeral value, because the result of a call can end up in the state. When the configuration of an ephemeral resource is unknown during a plan, the open waits for the apply. It is unknown when it refers to a resource that does not exist yet.

Environment variables

pravo reads an environment variable itself, with no provider, on every plan and apply. There are two kinds. A pravo.#Env is an ordinary value. It is recorded wherever it is used. A pravo.#SecretEnv is an ephemeral value. The same rules decide where it may go.

env.cue
package estate

import "pravo.dev/pravo"

region:  pravo.#Env & {name: "REGION", default: "eu-west-1"}
api_key: pravo.#SecretEnv & {name: "API_KEY"}

noted: $local.#Resource.local_file & {
	filename: "noted.txt"
	content:  "region = \(region.value)\n"
}

A variable that is not set and has no default is an error. A #SecretEnv has no default.

terminalexit 1
$env -u API_KEY pravo plan
Error: api_key: the environment variable API_KEY is not set

The value of region is an ordinary attribute of noted, so the state holds it.

terminal
$API_KEY=not-recorded pravo apply -auto-approve | grep '^Apply complete'
$grep -c "eu-west-1" pravo.state/resources/noted.json
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]
1

The kind is chosen where the variable is declared.

Write-only attributes

A write-only attribute is an attribute that the provider reads from the configuration and never stores. The generated schema marks it @pravo(write_only).

terminal
$pravo schema tls_self_signed_cert | grep write_only
	private_key_pem_wo?: string @pravo(sensitive,write_only)

An ephemeral value may be the value of a write-only attribute, because nothing about it reaches the state. The rule above treats a write-only attribute as the one position in a managed resource that accepts one.

cert.cue
package estate

key: $tls.#Ephemeral.tls_private_key & {algorithm: "ED25519"}

cert: $tls.#Resource.tls_self_signed_cert & {
	private_key_pem_wo:         key.private_key_pem
	private_key_pem_wo_version: 1
	subject: [{common_name: "example.test"}]
	validity_period_hours: 24
	allowed_uses: ["digital_signature"]
}

pravo validate accepts the specification.

terminal
$pravo validate
Success! 3 resources, 0 data sources, 3 ephemeral, 0 function calls, 2 outputs across 2 providers.

The provider plans a write-only attribute as null, so the plan has no line for private_key_pem_wo. The version attribute next to it is an ordinary attribute, and the plan shows it.

terminal
$API_KEY=not-recorded pravo plan | grep -E 'private_key|^Plan'
  +     private_key_pem_wo_version: 1
Plan: 1 to add, 0 to change, 0 to destroy.

The apply hands the key to the provider with the rest of the configuration, and the provider signs the certificate with it.

terminal
$API_KEY=not-recorded pravo apply -auto-approve | grep '^Apply complete'
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <id>]

The state holds null for the attribute, and the key is nowhere in it.

terminal
$pravo state show cert | grep private_key
$grep -q 'PRIVATE KEY' pravo.state/resources/cert.json || echo "not found"
	private_key_pem:            null
	private_key_pem_wo:         null
	private_key_pem_wo_version: 1
not found

A pravo.#SecretEnv is accepted in the same place. Keep secrets out of the state shows one. Remove the certificate before the next section.

terminal
$rm cert.cue
$API_KEY=not-recorded pravo apply -auto-approve | grep '^Apply complete'
Apply complete! Resources: 0 added, 0 changed, 1 destroyed. [txn <id>]

Sensitive attributes and the state

A provider marks an attribute sensitive in its schema. pravo prints it as (sensitive) in a plan. The state holds the attribute as the provider returned it. Here is the private key of deploy, in the record.

terminal
$pravo state show deploy | grep -c 'BEGIN OPENSSH PRIVATE KEY'
1

The printed plan hides it. The plan document does not.

terminal
$pravo plan -destroy | grep private_key
  # deploy (tls_private_key) will be destroyed
  -     private_key_openssh:           (sensitive)
  -     private_key_pem:               (sensitive)
  -     private_key_pem_pkcs8:         (sensitive)
terminal
$pravo plan -destroy --json 2>/dev/null | grep -c 'BEGIN OPENSSH PRIVATE KEY'
1

The plan document is the document that policies are judged against, and the document that --json hands to a program. A refusal by a policy quotes the offending value. Policies says what that means for a rule over a sensitive attribute.

The files of the state are written with mode 0644, as the umask allows. A saved plan is written with mode 0600, and it holds the prior and planned values of every change. Treat both as sensitive.

terminal
$ls -l p.plan | cut -c1-10
-rw-------

Outputs

An output with sensitive: true is hidden where outputs are listed. That is a plan, the summary of an apply, and pravo output without a name. The state still holds the value, wrapped as {"$sensitive": VALUE}. pravo output NAME and --json print it.

terminal
$pravo output | head -1
private: (sensitive)
terminal
$pravo output private | grep -c 'BEGIN PRIVATE KEY'
1

Another root reads outputs with pravo.#RemoteOutputs. It reads outputs.json alone, and an output that the producing root marked sensitive is not given out.

Recap

  • A value is recorded in the state, the saved plan and the plan document unless it is ephemeral. Sensitive hides a value on a screen and nowhere else.
  • An ephemeral value exists for one run. It may go into provider configuration, a write-only attribute and another ephemeral resource, and nowhere else.
  • pravo.#SecretEnv is an ephemeral environment variable, and pravo.#Env is an ordinary one.

Back to Concepts, or to the guide on secrets.