Deployment Guide¶
This guide covers how system administrators deploy and configure dotvault across an organisation.
Architecture overview¶
dotvault runs as a per-user daemon. Each user has their own instance, their own Vault identity, and their own secrets. The administrator's role is to:
- Set up the Vault infrastructure (KV engine, auth methods, policies)
- Deploy the dotvault binary to machines
- Distribute a configuration file (or Group Policy on Windows)
- Arrange for dotvault to start in each user's session
Vault infrastructure¶
KV engine¶
Enable KVv2 and create the user prefix namespace:
Policies¶
Create a template policy that scopes each user to their own secrets. See KV Engine & Policies for the full policy file.
Auth method¶
Enable and configure at least one auth method. OIDC is recommended for desktop environments as it integrates with existing SSO.
Configuration distribution¶
Linux¶
Place the config file at the system-wide location:
dotvault also checks paths listed in $XDG_CONFIG_DIRS.
Deploy with your existing configuration management (Ansible, Puppet, NixOS, etc.):
macOS¶
Place the config file at:
Deploy via MDM (Jamf, Munki) or configuration management.
Windows¶
Place the config file at:
Or use Group Policy to manage configuration centrally via the registry.
Registry takes precedence
On Windows, if Group Policy registry keys exist at HKLM\SOFTWARE\Policies\goodtune\dotvault, dotvault loads all configuration from the registry and ignores the YAML file entirely. The --config CLI flag is refused while a policy is present unless that policy opts in with a BypassSystemConfig REG_DWORD of 1 (the bypass_system_config: true equivalent); see Windows Group Policy.
Running as a user service¶
systemd (Linux)¶
Upgrading from a manually-created unit
Previous versions of this guide showed an example ~/.config/systemd/user/dotvault.service snippet. If you created one, remove it before enabling the packaged unit — the per-user path shadows /usr/lib/systemd/user/ and your hand-rolled unit (which lacks Type=notify, WatchdogSec, the env-file paths, etc.) will silently take precedence:
rm ~/.config/systemd/user/dotvault.service
systemctl --user daemon-reload
systemctl --user enable --now dotvault.service
Behavioural change to be aware of: services declaring After=dotvault.service now block until dotvault completes its initial sync (the packaged unit uses Type=notify and delays READY=1 until secrets are on disk). The previous hand-rolled unit had no readiness gate, so dependents started in parallel. If a dependent's startup ordering matters to you, this is the change to plan for.
The RPM, DEB, and APK packages all ship a dotvault.service user unit (a Type=notify service with WatchdogSec=120 and the OpenTelemetry-friendly logging settings) at the canonical /usr/lib/systemd/user/ path. dotvault is a per-user daemon — it authenticates to Vault with the OS user's identity and writes secrets into that user's $HOME — so installing it as a system service that runs as root would write to root's $HOME and authenticate to Vault as root, which is almost never what you want.
Enable per-user once the package is installed:
The daemon watches ~/.dotvault-token itself (via inotify on Linux), so subsequent rewrites of the file (typically from an interactive dotvault login in another shell) trigger an immediate token re-read on the running daemon within seconds — no extra unit to enable. See Config reload for the full mechanism.
Or enable globally for every login session on the machine:
--global enables the unit in every user's session; each user runs their own instance and authenticates with their own Vault identity.
Enable lingering if the daemon must outlive a login session
A --user service normally stops when the user's last session ends, and $XDG_RUNTIME_DIR (where the SSH agent and local API socket live) is torn down with it. For a machine people reach over SSH — where a tmux job or the local API socket is expected to survive a disconnect — enable lingering so the user manager keeps running:
Environment-variable overrides (e.g. OTEL_EXPORTER_OTLP_ENDPOINT) can be set via four optional EnvironmentFile= paths referenced by the unit:
~/.config/dotvault/env(preferred for per-user secrets)~/.config/dotvault.env/etc/default/dotvault/etc/sysconfig/dotvault
The system-wide paths are typically world-readable, so the per-user ~/.config/dotvault/env is the right place for anything sensitive (e.g. an OTLP bearer token in OTEL_EXPORTER_OTLP_HEADERS). Create the file with chmod 600; all four are silently ignored if absent.
%h vs ~ in custom unit drop-ins
The packaged unit references the per-user paths as %h/.config/dotvault/env and %h/.config/dotvault.env. %h is systemd's home-directory specifier — equivalent to ~ when you're creating the file at the shell. If you reference the file from a systemctl --user edit drop-in or a custom unit, write %h (or ${HOME}); systemd does not expand ~ in EnvironmentFile= directives, so a literal ~/.config/... would be silently skipped.
The unit hard-codes a couple of system paths that the package owns: ExecStart=/usr/bin/dotvault run, plus the EnvironmentFile= paths listed above. If you install dotvault into a non-standard location (e.g. /usr/local/bin), copy the unit out to ~/.config/systemd/user/dotvault.service and adjust those lines.
Slow initial sync and the systemd startup window
With Type=notify, two different deadlines govern dotvault's lifecycle:
TimeoutStartSec— the pre-READY=1window. systemd waits this long for the daemon to finish auth + initial sync and signal ready. The packaged unit sets it to 300 seconds; the systemd default of ~90s is too tight for resource-constrained hosts (many rules, slow Vault, cold TLS handshake). If the daemon doesn't reachREADY=1in time, systemd marks the start a failure and restarts — causing a boot loop on chronically slow hosts.WatchdogSec— the post-READY=1liveness check. The daemon kicks the watchdog at half this interval after becoming ready; if the kicks stop, systemd restarts the unit. The packaged unit sets it to 120 seconds.
WatchdogSec does not extend the startup window — only TimeoutStartSec does. To raise the startup window (or the watchdog) on a host where the defaults are too tight, use a drop-in:
systemctl --user edit dotvault.service
# Under [Service], one or both of:
# TimeoutStartSec=600
# WatchdogSec=300
TimeoutStartSec=infinity disables the pre-ready timeout entirely if your environment can't bound the first sync.
Note also that anything declaring After=dotvault.service now blocks until the first sync completes — a behavioural change from the previous manually-created unit which had no Type=notify gate.
launchd (macOS)¶
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.goodtune.dotvault</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/dotvault</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/dotvault.err</string>
</dict>
</plist>
Deploy to /Library/LaunchAgents/ (all users) or ~/Library/LaunchAgents/ (single user).
Windows Task Scheduler¶
Create a scheduled task that runs at user logon:
$action = New-ScheduledTaskAction -Execute "C:\Program Files\dotvault\dotvault.exe" -Argument "run"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "dotvault" -Action $action -Trigger $trigger -Settings $settings
Or deploy via Group Policy as a scheduled task.
Logging¶
dotvault writes all logs to stderr:
- Text format when stderr is a TTY (interactive use)
- JSON format otherwise (service/daemon use)
Control verbosity with --log-level:
Available levels: debug, info (default), warn, error.
Override the auto-selected format with --log-format:
dotvault run --log-format json # force structured logs
dotvault run --log-format text # force human-readable logs
dotvault run --log-format auto # default — text on TTY, JSON otherwise
This is useful when running under a service manager that captures stderr but is connected to a TTY for debugging, or when forcing structured logs for ingestion into a log collector regardless of how the daemon was launched.
There is no file-based logging — integrate with your platform's log collection (journald, syslog, Windows Event Log via a wrapper, etc.). On systemd hosts the packaged unit routes stderr to the journal, so the OpenTelemetry collector's journaldreceiver can filter on _SYSTEMD_USER_UNIT=dotvault.service (or _SYSTEMD_UNIT when the unit was enabled with systemctl --global) to pick logs up directly.
Observability¶
dotvault can export OpenTelemetry metrics and logs to a local OTel collector — a single observability: block in config.yaml drives both signals against the same endpoint. Disabled by default; enable with:
observability:
enabled: true
endpoint: "127.0.0.1:4317" # local OTel collector
protocol: "grpc" # or "http/protobuf"
insecure: true # disable TLS for the local hop
export_interval: "15s"
# headers:
# authorization: "Bearer …"
For http/protobuf, set endpoint to a base URL like https://otel.example — the SDK appends /v1/metrics and /v1/logs itself. A URL that already includes a signal-specific path (e.g. ending in /v1/metrics) routes both signals to the same wrong route.
Windows Group Policy
The observability block round-trips through the GPO/registry layer like every other section — author it under SOFTWARE\Policies\goodtune\dotvault\Observability (or generate the values with dotvault reg-import). Header values round-trip too (as REG_SZ values under Observability\Headers), so a .reg export carries the live tokens — treat the artefact as a secret. To keep tokens out of the policy hive and out of any exported config, leave headers empty and set them via the standard OTEL_EXPORTER_OTLP_HEADERS environment variable (through a machine-wide environment policy) instead. See Windows Group Policy for the full registry schema.
The standard OTEL_* environment variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, …) are also honoured by the SDK, so the endpoint/headers fields can be left empty and managed centrally via /etc/default/dotvault.
The exporter emits a bounded set of instruments:
| Metric | Type | Attributes |
|---|---|---|
dotvault.sync.ticks |
counter | outcome={ok,error} |
dotvault.sync.duration |
histogram | outcome |
dotvault.vault.calls |
counter | op={read,write,lookup_self,renew_self}, status |
dotvault.token.renewals |
counter | outcome={renewed,reauth_required,failed} |
dotvault.token.ttl_remaining |
histogram | (no attrs) |
dotvault.enrol.attempts |
counter | engine, outcome={completed,error} |
dotvault.web.requests |
counter | route, status_class={1xx…5xx} |
dotvault.config.reloads |
counter | outcome={no_change,applied,error} |
dotvault.sighup.received |
counter | (no attrs) — each SIGHUP forces an immediate ~/.dotvault-token re-read and config reload |
Log records¶
The OTel logs exporter is not a wholesale replacement for stderr — operational logging still goes through log/slog to stderr / journald. The OTel logger is reserved for deployment-fact records that should reach a central collector but must not noise up an end user's terminal. Currently the only emit is:
configuration loaded from Windows Registry (Group Policy); file-based config is ignored— WARN severity, attributepath=<would-be config file>. Fires once per daemon/sync startup on a GPO-managed Windows box. Replaces the per-invocationslog.Infoline that previously leaked onto stdout for every CLI invocation on a GPO-managed install.
Health probes are served on whichever HTTP surfaces the daemon has: the loopback listener when web.enabled: true, and/or the per-user Unix socket when api.enabled: true. A deployment with neither enabled has nothing to probe; enable one of them, or rely on the systemd sd_notify(READY=1) signal instead. The OTel httpcheckreceiver speaks TCP, so it needs web.enabled; a socket-only daemon is probed with curl --unix-socket <path> http://localhost/readyz, which is the way to get a readiness check without opening a port at all.
GET /healthz— liveness, always 200 while servingGET /readyz— readiness, 200 once the daemon is authenticated to Vault AND has completed its initial sync cycle, 503 otherwise. Mirrors thesd_notify(READY=1)contract so a KubernetesreadinessProbeor the OTelhttpcheckreceivernever observes a green daemon before secrets exist on disk. The auth check reflects the cached in-memory token, not a per-probe Vault round-trip; a revoked token flips/readyzback to 503 within the lifecycle check cadence (default 5 min).
Both return JSON and are loopback-only, suitable for the OTel httpcheckreceiver.
Security considerations¶
- File permissions — all managed files are written with
0600. dotvault warns if the config file is group or world writable. - Token security —
~/.dotvault-tokenis written with0600. Secret values are never logged, even at debug level. dotvault uses this dotvault-specific filename rather than Vault's default~/.vault-tokenso a concurrentvaultCLI session cannot clobber the daemon's cached token.
Transitional — upgrading from v0.19.0 or earlier
Releases before v0.20.0 used Vault's default ~/.vault-token. There is no migration: dotvault re-authenticates once on first start, and any token it previously wrote to ~/.vault-token lingers at 0600 until it expires. If dotvault was the only writer of that file, delete it after upgrading. This note will be removed in a future release (around v0.23.0).
- Atomic writes — all file writes use temp file + rename to prevent partial writes.
- Web UI — loopback only, CSRF-protected, strict Content Security Policy.
- Windows — DACL-based permission checks via the Windows Security API.
Config reload¶
SIGHUP is the running daemon's reload trigger. It does two things at once: re-reads ~/.dotvault-token immediately (picking up a token freshly written by dotvault login), and re-runs the configuration loader immediately instead of waiting for the next config-refresh tick.
What a reload can and cannot apply:
- Applied in place — the dynamic sections:
rules,enrolments,sync.interval, andremote_configitself (the overlay fetcher is rebuilt and the refresh cadence re-derived on the next pass; note that a remote document still cannot carryremote_config— the section is local-only). These are the same sections the daemon already re-reads periodically on its config-refresh tick (default: the sync interval; seeremote_config.refresh_interval), whether the change came from an edited local config or the remote overlay. The signal just skips the wait. - Restart required — the static sections:
vault,web,api,agent,observability, and the top-levelbypass_system_configflag. These configure subsystems constructed once at startup (the Vault client, the web listener, the local API socket, the SSH agent, the OTel exporter). A reload that finds them changed logs a warning naming the changed sections; restart the daemon (systemctl --user restart dotvault.service) to apply them.
The packaged systemd unit wires SIGHUP as ExecReload=, so the canonical gesture on Linux is:
This targets the unit's MainPID specifically — preferable to kill -HUP $(pgrep -x dotvault), which would also signal any unrelated dotvault sync or go run ./cmd/dotvault invocation the user happens to be running (SIGHUP's default disposition is to terminate, so those side processes would die). On macOS the equivalent targeted form is launchctl kill SIGHUP gui/$(id -u)/com.goodtune.dotvault.
On Windows, SIGHUP is never delivered to processes. The system-tray icon (installed by both dotvault.exe run and dotvaultw.exe) carries a Reload config menu entry that performs exactly the same token re-read + immediate config reload; static-section changes log the same restart-required warning. Alternatively, changes to the dynamic sections still converge on the next config-refresh tick with no action at all.
Token re-read is automatic on Linux
On Linux the daemon also watches ~/.dotvault-token directly with inotify and re-reads it the moment the file is created or replaced — so when an interactive dotvault login writes a fresh token, the running daemon picks it up within seconds without any signal. This is built into the daemon (internal/tokenwatch); there is no separate unit to enable, and it works regardless of how dotvault was started. Deletes are ignored — the daemon keeps using its current in-memory token until a replacement is written. The watcher is a no-op off Linux; operators who want automatic re-read on macOS should script the launchctl kill form above on a launchd WatchPaths trigger.
Earlier releases shipped a dotvault-token-watch.path user unit that achieved the same nudge by SIGHUP-ing the daemon. It has been removed; the package upgrade deletes the unit files, but an enabled symlink left in ~/.config/systemd/user/ by a previous systemctl --user enable will persist and keep firing a (now redundant, but harmless) SIGHUP. After upgrading, clear it with systemctl --user disable --now dotvault-token-watch.path.