profiles
The Profiles engine processes declarative API requests with dynamic data source evaluation. The system implements a request/response pattern, supporting chained operations, allowing multiple API calls from a single entry point. This can be parsed from HCL or JSON as desired.
Data sources are dependent on the profile call site: self-initialization has one set of allowed sources whereas API profiles have a different combination. Data sources can be nested; for instance, a template source can be used to combine an environment variable and the contents of a file.
All requests are subject to the same restrictions as if they were executed normally: audit logging, authentication, and API variable restrictions (such as unauthenticated rotation and audit device creation) still apply.
Profiles end to end workflow
- Profile Creation
A privileged operator designs the profile; this involves creating a configuration which describes what API endpoints to call. If this profile is executed from the API profile system, it also involves defining input and output parameters.
This configuration either ends up in one or more files (if using declarative
self-initialization) or in a profile under the sys/profiles/manage/
endpoint.
- Input Validation
During execution, before any OpenBao operation is attempted, the engine:
- Ensures every declared parameter is present and of the correct type, e.g., string, int, bool.
- Confirms that each referenced source (environment variable, file path, earlier request, earlier response) is available.
- Rejects a run immediately if required inputs are missing, preventing half-configured states.
Certain engines, like template or CEL cannot fully validate the execution environment and thus may err at runtime.
- Synchronous Execution
Requests are executed exactly in the order they are listed. Within a single profile run, there is no parallelism, guaranteeing that any request or response references always point to data that already exists. Additionally, requests are fully synchronous and are not executed in the background.
If request fails unexpectedly, the engine stops immediately and returns an error to the caller; nothing after that point is attempted.
History of all requests and responses are recorded, though, are not exposed to the caller by default.
- Output Generation
Depending on context, the profile system may output information back to the caller. In the case of self-initialization, this information is logged at the Trace level; this will contain privileged information and should not be exposed to arbitrary users.
Request format
The core of a profile is the request, which contains the following fields:
name(string: <required>)- the human-readable name of this request. This is specified via a key to the HCL request block.operation(string: <required>)- the operation type to invoke. This aligns with ACL operations.path(string: <required>)- which API endpoint to invoke.token(string: <optional>)- the OpenBao token to use to authenticate with; in some cases (like self-initialization or authenticated execution of API profiles), this is inferred from the context automatically. However, in the case of unauthenticated execution of API profiles, this would need to be manually handled as none exists in the execution environment. Unauthenticated APIs (such as login requests) can continue to be called without this parameter or with this parameter explicitly set to the empty string.data(map[string]any: <optional>)- any parameters for this request.headers(map[string][]string: <optional>)- any headers for this request.allow_failure(bool: false)- whether to allow profile execution to continue despite failure of this request.when(bool: true)- when false, skips execution of the request.
Other than name which must be static, all of the remaining fields can use
dynamic data sources as described below.
In HCL this translates to:
request "create-admin-policy" {
operation = "create"
path = "/sys/policies/acl/admin"
token = "my-root-token"
data = {
policy = "my-acl-policy"
}
}
Logical groupings
Multiple requests can be grouped into a logical organization structure in certain profile execution environments:
initialize-- in declarative self-initializationcontext-- in API profiles
When doing so, the name of this outer grouping block is used for the request
and response dynamic data source and must be specified.
Output
When using API profiles, output returned to the caller must be specified in
an output block. This takes the following values:
data-(map[string]any: <optional>)- any output information to be returned to the caller.headers-(map[string][]string: <optional>)- any response headers to be included.
Note that both of these fields can use dynamic data sources to reference earlier responses.
Value types and dynamic data sources
Values may be of the following types:
-
A literal, such as a
stringormap. For example the following are valid literals:path = "sys/audit/stdout"or
data = {type = "file"} -
A data source evaluation. These take the form:
{eval_source = "<source name>"eval_type = "<resulting type>"... additional source-specific parameters ...}where
eval_source(string: <required>)- the name of the given source; valid values are defined below.eval_type(string: <required>)- the output type after evaluation; recognized Go types arestring,int,float64,bool,[]string,map(alias formap[string]interface{}), andany(alias forinterface{}). If the output of the source evaluation is not convertible to the desired type, a runtime error will occur.
Note that fields within a source evaluation statement may not be themselves be source evaluations.
env source
The env source is only available for declarative self-initialization; it
is not available for API profiles
env_var(string: <required>)- the name of an environment variable to return the value of.require_present(bool: false)- when asserted, require that the named environment variable must be present and err otherwise.
For example, to reference an initial admin password from an environment variable:
request "userpass-add-admin" {
operation = "update"
path = "auth/userpass/users/admin"
data = {
"password" = {
eval_type = "string"
eval_source = "env"
env_var = "INITIAL_ADMIN_PASSWORD"
require_present = true
}
"token_policies" = ["superuser"]
}
}
file source
The file source is only available for declarative self-initialization; it
is not available for API profiles
path(string: <required>)- the path to the file to read. This file is only ever read once and is closed after use; thus a single piece of data could be provided on/dev/stdin. Errs if the file is not readable.
For example, to load a X.509 root CA into a PKI engine:
request "provision-root-ca" {
operation = "update"
path = "pki/issuers/import/cert"
data = {
"pem_bundle" = {
eval_type = "string"
eval_source = "file"
path = "/data/root-ca.pem"
}
}
}
request source
<outer-block>_name(string: <required>)- name of the outer block block to reference.<outer-block>is the type of the outer block; for self-init, this isinitialize_name; for profiles, this iscontext_name.request_name(string: <required>)- name of the request block inside the initialize block to reference. Must have already been executed.field_selector(string or []string: <required>)- field within the request to reference; for nesting, specify multiple values as a list. The request is marshalled directly; for example, to reference the path, usefield_selector = "path"; to reference a specific field of input data, usefield_selector = ["data", "my_top_level_field"].
For example, to reference a previous request's input for a subsequent call:
initialize "namespace-identity" {
request "namespace-userpass-add-admin" {
operation = "update"
path = "ns1/auth/userpass/users/admin"
data = {
"password" = {
eval_type = "string"
eval_source = "request"
initialize_name = "identity"
request_name = "userpass-add-admin"
field_selector = ["data", "password"]
}
"token_policies" = ["superuser"]
}
}
}
response source
<outer-block>_name(string: <required>)- name of the outer block block to reference.<outer-block>is the type of the outer block; for self-init, this isinitialize_name; for profiles, this iscontext_name.request_name(string: <required>)- name of the request block inside the initialize block, to reference the corresponding response of. Must have already been executed.field_selector(string or []string: <required>)- field within the response to reference; for nesting, specify multiple values as a list. The response is marshalled directly; for example, to reference a specific field of output data, usefield_selector = ["data", "my_top_level_field"].
For example, to reference a login token in a subsequent call:
initialize "example" {
request "use-auth" {
operation = "update"
path = "sys/namespaces/ns1"
token = {
eval_type = "string"
eval_source = "response"
initialize_name = "authenticate"
response_name = "admin-userpass"
field_selector = ["auth", "client_token"]
}
data = {}
}
}
cel source
expression(string: <required>)- the CEL expression to evaluate; errs if not present.variables([]map["name": string, "expression": string]: <optional>): a list of variables to evaluate and inject into the program's context; each entry in the list is a map containing two keys:name(string), the name of the variable,expression(string), the CEL expression to evaluate for this variable's value.
Additionally, the following reserved values are injected into the CEL context when allowed as sources:
requests, if therequestsource is allowedresponses, if theresponsesource is allowedinput, if theinputsource is allowed
For example, to sum the value of two responses:
request "sum" {
operation = "update"
path = "secret/data/my-secret"
data = {
"secret_number" = {
eval_type = "int"
eval_source = "cel"
expression = "requests.first.read.my_field + requests.second.read.my_other_field"
}
}
}
template source
template(string: <required>)- thetemplateexpression to evaluate; errs if not present.data(map[string]interface{}: <optional>): additional context for the templating engine.
Additionally, the following reserved values are injected into the CEL context when allowed as sources:
requests, if therequestsource is allowedresponses, if theresponsesource is allowedinput, if theinputsource is allowed
For example, inject the hash of a password:
request "password" {
operation = "update"
path = "secret/data/my-secret"
data = {
pem_bundle = {
eval_type = "string"
eval_source = "template"
template = "{{ .responses.database.read-password.password | sha256 }}"
}
}
}
input source
The input source is only available for API profiles; it is not available
for declarative self-initialization.
field_name(string: <required>)- the field of the input request parameter to read.
For example, if the parameter password was specified in a profile:
context "provision" {
request "password" {
operation = "update"
path = "secret/data/my-secret"
data = {
"pem_bundle" = {
eval_type = "string"
eval_source = "input"
template = "{{ .input.password }}"
}
}
}
}