Getting started
This walk-through enrolls one host, writes one policy, and binds one container. Use the controller web interface for policy and binding changes; run the host and container commands where indicated.
Enroll the agent
On the host, point the agent at the controller. The command generates the agent’s key pair, prints its fingerprint, and shows the controller’s host-key fingerprint for you to accept.
sudo wyrwood agent login ssh://controller.example.internal:2222
sudo wyrwood agent service start
Sign in to the controller’s web interface through your identity provider, open Agents, and approve the pending key after comparing its fingerprint with the one the login command printed. The host’s containers appear once the agent reports in. For a controller that also runs its local agent, follow Controller with a local agent to enroll and approve that host; do not start a separate agent daemon.

Write a policy
Open Policies → New policy and enter a name, such as teal.
Use Graphical paths to add paths and select their capabilities, or
Raw HCL to paste the example below. Select the SSH identities and keep
the default contract for this example. Choose Save and validate and check
that OpenBao accepts the policy. A policy has three parts:
-
The OpenBao policy, in HCL. For example, read access to one KV path and signing with one Transit key. Use Agent token renewal to control automatic renewal. Wyrwood supplies the lifecycle grants:
path "kv/data/teams/teal/*" { capabilities = ["read"] } path "transit/sign/teal-github" { capabilities = ["update"] } path "transit/keys/teal-github" { capabilities = ["read"] } -
The SSH identities the agent socket should offer. Name the Transit key
teal-github. Name an SSH-engine role if a host you reach trusts the OpenBao CA. -
The contract. The default mounts the consumer directory at
/run/wyrwoodand setsSSH_AUTH_SOCKandVAULT_ADDRto the sockets inside it.
When binding, choose Access group from the groups reported by the running
host agent. The container processes must retain that numeric GID as a primary
or supplementary group. A capable agent also offers a custom numeric GID.
See Access group for stale reports and host
group changes. Leave binding enforcement at report unless you want one
recreate with the mount applied on first bind. Preview the effective policy
before saving. TTL, access group, and enforcement are binding settings.
An access group is effectively required for non-root containers running as
another UID than the agent. Without it, the agent owns the directory (0700)
and sockets (0600); with it, their modes are 0750 and 0660.
On each host, add the agent user to the chosen group and restart the agent. For access group 1000:
getent group 1000
sudo usermod -aG <host-group-name> wyrwood
sudo systemctl restart wyrwood-agent
Replace <host-group-name> with the host group name for GID 1000. Create a
host group with that GID first if none exists. Use a separate group from
docker; the agent has no direct Docker access. Members of the access group
who can reach these sockets can use the
binding’s OpenBao and SSH permissions. See Access group.
After you bind a container, the policy’s Bindings tab lists the binding. A
binding stays on that list, marked Not reported, when its
container no longer exists, so you can see what a policy change affects.


Bind a container
Open Bindings and select Add binding. The screen lists the reported containers that have no binding.

To bind a container before its agent reports it, select Bind by typed key and give its key and directory name. See Bind a container before it exists.
Select Add binding on the container’s row. Its bind screen opens, with its actions at the top and a summary of what you are about to save. Select one or more policies on Key and policies, and change any defaults on Settings. Use Preview effective policy to inspect the combined policy, then Bind policies to save. You do not need to open every tab. If the controller refuses the binding, the same screen shows why, selects the tab that holds the problem, and keeps what you entered. The controller binds by the most stable key the container offers and shows which one it used. The agent creates the container’s consumer directory, obtains its OpenBao token, and checks the contract.


A container created without the mount is reported as non-conforming. Either
add the mount and environment to the container’s definition and recreate it
yourself, or set the binding’s enforcement to recreate and bind again. A
recreate stops, removes, and creates the container once. Anything outside a
mount or volume is discarded, and an attached editor session reconnects.
For a manual recreate, use this order:
- Bind the existing container and wait for the agent to create
/run/wyrwood/containers/<container-name>on the host. Use the container’s Docker name for this path, including when the binding uses a label key. - Add the mount and environment variables to the replacement definition.
Use
--mount type=bindso Docker fails if the source directory is absent. - Remove the old container and create the replacement with the same Docker name and binding key. Keep the Docker name even when the binding uses a label key: the consumer directory is named after the container. The agent keeps that directory through the gap and restores its sockets when the replacement receives its binding.
For example, replace a container named app:
docker rm -f app
docker run -d --name app \
--mount type=bind,src=/run/wyrwood/containers/app,dst=/run/wyrwood \
-e SSH_AUTH_SOCK=/run/wyrwood/ssh-agent.sock \
-e VAULT_ADDR=unix:///run/wyrwood/vault.sock \
alpine sleep infinity
Keep the image, other mounts, labels, and options your application needs.
Avoid -v for this mount: Docker creates a missing source as root, and the
agent rejects directories owned by another user.
If Docker has already created a root-owned source, stop the container before repairing it. Check that the directory is empty, then replace it with one owned by the agent service user. For the default service configuration without an access group:
docker stop app
sudo rmdir /run/wyrwood/containers/app
sudo install -d -o wyrwood -g wyrwood -m 0700 /run/wyrwood/containers/app
docker start app
Stop if rmdir fails; do not recursively delete the directory. The agent applies
its configured access group when it opens the repaired directory. Repair before
starting the container so its bind mount refers to the new directory.

Bindings then shows the binding’s state in the list. Select Edit on the row to return to this screen.

For a devcontainer named app, set the Docker name explicitly so rebuilds
reuse its consumer directory. Use the existing container’s Docker name in both
runArgs and the mount source, and choose a name unique on that Docker host:
{
"runArgs": ["--name", "app"],
"mounts": [
"source=/run/wyrwood/containers/app,target=/run/wyrwood,type=bind"
],
"containerEnv": {
"SSH_AUTH_SOCK": "/run/wyrwood/ssh-agent.sock",
"VAULT_ADDR": "unix:///run/wyrwood/vault.sock"
}
}
Use the sockets
Inside the container:
bao kv get kv/teams/teal/example
ssh-add -L
git -c core.sshCommand=ssh push
The first command carries the container’s token without the container ever seeing it. The second lists the Transit key. The third signs the push in OpenBao.