Skip to content

Templates

Templates let you reshape Vault secret data before it is written to a target file. They use Go's text/template syntax and receive the Vault KV data map as the dot (.) context.

When no template is specified, the raw Vault secret data is passed directly to the format handler.

Template functions

In addition to the standard Go template functions, dotvault provides:

Function Signature Description
env env(key) Look up an environment variable
base64encode base64encode(s) Base64-encode a string
base64decode base64decode(s) Base64-decode a string
default default(fallback, val) Return val if non-empty, otherwise fallback
quote quote(s) Shell-safe single quoting
username username The local OS account the secrets are synced under

The default function follows the Sprig convention where the fallback comes first, enabling idiomatic piping:

{{ .port | default "8080" }}

The username function returns the OS account dotvault runs as — the same identity the kv/users/<username>/… path layout is built from (DOMAIN\ prefix stripped on Windows). It is a function, not a field, so it is always available regardless of the secret's contents and never collides with a secret field that happens to be named user. Use it to build per-user filesystem paths without storing the username in Vault:

RemoteForward /home/{{ username }}/.ssh/dotvault.sock 127.0.0.1:8200

Examples by format

YAML

Sync a GitHub CLI credential into ~/.config/gh/hosts.yml:

rules:
  - name: gh
    vault_key: "gh"
    target:
      path: "~/.config/gh/hosts.yml"
      format: yaml
      template: |
        github.com:
          oauth_token: "{{ .oauth_token }}"
          user: "{{ .user }}"
          git_protocol: https

If the user already has other entries in hosts.yml (e.g. for GitHub Enterprise), those entries are preserved. Only the github.com key is merged.

Vault secret (kv/data/users/jane/gh):

{ "oauth_token": "ghp_xxxx", "user": "jane" }

Result (merged with existing file):

github.com:
  oauth_token: "ghp_xxxx"
  user: "jane"
  git_protocol: https
github.enterprise.com:
  oauth_token: "gho_yyyy"   # preserved from existing file

JSON

Sync database credentials into a JSON config file:

rules:
  - name: db-config
    vault_key: "db"
    target:
      path: "~/.myapp/config.json"
      format: json
      template: |
        {
          "database": {
            "host": "{{ .host | default "localhost" }}",
            "port": {{ .port | default "5432" }},
            "username": "{{ .username }}",
            "password": "{{ .password }}"
          }
        }

Vault secret:

{ "host": "db.internal", "port": "5432", "username": "jane", "password": "s3cret" }

Result (merged with existing file):

{
  "database": {
    "host": "db.internal",
    "port": 5432,
    "username": "jane",
    "password": "s3cret"
  },
  "logging": {
    "level": "info"
  }
}

The logging section from the existing file is preserved.

TOML

Sync credentials into a TOML configuration:

rules:
  - name: cargo-registry
    vault_key: "cargo"
    target:
      path: "~/.cargo/credentials.toml"
      format: toml
      template: |
        [registries.my-registry]
        token = "{{ .token }}"

Vault secret:

{ "token": "cargo_xxxxxxxxxxxx" }

Result:

[registries.my-registry]
token = "cargo_xxxxxxxxxxxx"

[registries.crates-io]
token = "existing_token"    # preserved from existing file

INI

Sync AWS credentials into ~/.aws/credentials:

rules:
  - name: aws-creds
    vault_key: "aws"
    target:
      path: "~/.aws/credentials"
      format: ini
      template: |
        [default]
        aws_access_key_id = {{ .access_key }}
        aws_secret_access_key = {{ .secret_key }}

Vault secret:

{ "access_key": "AKIAXXXXXXXX", "secret_key": "wJalrXxxxxxxxx" }

Result (merged with existing file):

[default]
aws_access_key_id = AKIAXXXXXXXX
aws_secret_access_key = wJalrXxxxxxxxx

[profile staging]
aws_access_key_id = AKIA_OTHER    # preserved from existing file
aws_secret_access_key = other_key

Netrc

Sync machine credentials into ~/.netrc:

rules:
  - name: netrc
    vault_key: "netrc"
    target:
      path: "~/.netrc"
      format: netrc
      template: |
        machine github.com
          login {{ .user }}
          password {{ .oauth_token }}

Vault secret:

{ "user": "jane", "oauth_token": "ghp_xxxx" }

Result (merged with existing file):

machine github.com
  login jane
  password ghp_xxxx

machine gitlab.com
  login jane
  password glpat-yyyy

Entries are merged by machine name. The existing gitlab.com entry is preserved.

Text (plain)

Sync a private key or certificate:

rules:
  - name: ssh-key
    vault_key: "ssh"
    target:
      path: "~/.ssh/id_ed25519"
      format: text
      template: "{{ .private_key }}"

Vault secret:

{ "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3Blbn..." }

Text format uses full replacement — the entire file content is overwritten. This is appropriate for opaque blobs like private keys and certificates where merging is not meaningful.

ssh_config

Surgically manage directives in ~/.ssh/config. The motivating case is exposing the local dotvault (Vault) endpoint on a remote host through a stable, per-user unix socket — one half of a dotvault-to-dotvault information-sharing setup. A RemoteForward creates the remote socket and forwards it back to 127.0.0.1:8200 on the originating machine:

rules:
  - name: dotvault-forward
    target:
      path: "~/.ssh/config"
      format: ssh_config
      template: |
        Host *
            User {{ username }}
            RemoteForward /home/{{ username }}/.ssh/dotvault.sock 127.0.0.1:8200

This rule has no vault_key — an ssh_config has no Vault-backed secrets, so the rule is keyless: dotvault never contacts Vault for it and renders the template with an empty data context. The path-building username comes from the username function (the OS account dotvault runs as), which resolves regardless because it is a template function, not a context field. dotvault matches the Host * section by its criteria line and updates only the User and RemoteForward directives inside it. Every other section, comment, and directive in the file is preserved verbatim. Because {{ username }} is stable across syncs, the RemoteForward listen path stays constant and the directive updates in place instead of accumulating duplicates. (A rule may still set vault_key if its template needs secret fields — vault_key is simply optional. See Sync rules → keyless rules.)

Keep a forward's listen path stable. A RemoteForward is identified for merge by its listen spec (the first argument, /home/{{ username }}/.ssh/dotvault.sock here). If that path ever renders differently from the line already in the file, dotvault sees a new forward, appends it, and leaves the old one behind — so a forward whose listen path you change is added rather than rewritten, and you remove the stale line by hand once. This is why the path uses the stable {{ username }} function: a template that previously rendered the path with a different (or empty) value will not match and will orphan. See Sync rules → ssh_config for the full discriminator semantics.

The ssh_config format is template-only — there is no raw-data fallback, so the template field is required (see below).

Templates without the template field

If no template is specified, dotvault passes the raw Vault KV data map to the format handler. For YAML and JSON, this means all fields from the Vault secret are written to the file:

rules:
  - name: app-secrets
    vault_key: "myapp"
    target:
      path: "~/.myapp/secrets.yaml"
      format: yaml

If the Vault secret at kv/data/users/jane/myapp contains {"api_key": "xxx", "db_pass": "yyy"}, the resulting file would have both fields merged into it.

The ssh_config format is the exception: it has no raw-data path (there is no sensible mapping from arbitrary key/value pairs to ssh directives), so a rule using it must always supply a template. Omitting it produces a clear error at sync time.

Tips

  • Use default to provide fallback values for optional fields
  • Use base64encode for credentials that need to be base64-encoded in the target format (e.g. Kubernetes secrets)
  • Use quote when embedding values in shell scripts or contexts where quoting matters
  • Use env sparingly — it reads from the dotvault process environment, not the user's shell