Installation
Set up the repo.wyrd.foo APT repository (once)
sudo install -d -m 0755 /etc/apt/keyrings curl -fsSL https://repo.wyrd.foo/pubkey.gpg | sudo tee /etc/apt/keyrings/wyrd-company.gpg >/dev/null echo "deb [signed-by=/etc/apt/keyrings/wyrd-company.gpg] \ https://repo.wyrd.foo/apt stable main" | sudo tee /etc/apt/sources.list.d/wyrd-company.list >/dev/null sudo apt update
sudo apt install wyrwood Install the controller once per site and the agent on every host that runs containers.
Controller
The controller is published as a container image. It needs a dedicated OpenBao namespace, an AppRole in that namespace, an OpenID Connect client for operator sign-in, a state volume, and a configuration file. The AppRole secret identifier and the client secret are read from mounted files.
OpenBao
Create the namespace and its mounts with an OpenBao administrator token.
Policies and contracts explains why the
namespace is required and gives the controller policy; save it as
controller.hcl.
bao namespace create wyrwood
export BAO_NAMESPACE=wyrwood
bao auth enable approle
bao secrets enable -version=2 kv
bao secrets enable transit
bao secrets enable ssh
bao write ssh/config/ca generate_signing_key=true key_type=ed25519
bao policy write controller controller.hcl
bao write auth/approle/role/controller token_policies=controller token_ttl=1h
bao read -field=role_id auth/approle/role/controller/role-id
bao write -f -field=secret_id auth/approle/role/controller/secret-id
Keep the secrets your containers read in this namespace.
Controller container
The image runs as user and group 65532. State lives in a named volume, which Docker creates with that ownership from the image. The two secret files must be readable by that user.
In your identity provider, for example Pocket ID, create a confidential
client with PKCE, set its redirect URL to
https://<controller public host>/auth/callback, and create the group whose
members may operate Wyrwood.
mkdir -p ~/wyrwood
# Paste each secret when prompted, then press Ctrl-D.
cat > ~/wyrwood/secret-id
cat > ~/wyrwood/oidc-client-secret
sudo chown 65532:65532 ~/wyrwood/secret-id ~/wyrwood/oidc-client-secret
sudo chmod 0400 ~/wyrwood/secret-id ~/wyrwood/oidc-client-secret
cat > ~/wyrwood/controller.yml <<'YAML'
http:
# Inside the container; the published port below keeps it on host loopback.
listen: 0.0.0.0:8080
ssh:
listen: 0.0.0.0:2222
openbao:
address: https://openbao.example.internal:8200
namespace: wyrwood
approle:
role-id: REPLACE_WITH_ROLE_ID
secret-id-file: /run/secrets/openbao-secret-id
oidc:
provider-name: Pocket ID
issuer: https://id.example.internal
client-id: REPLACE_WITH_CLIENT_ID
client-secret-file: /run/secrets/oidc-client-secret
redirect-url: https://wyrwood.example.internal/auth/callback
allowed-groups:
- wyrwood-operators
YAML
docker run -d --name wyrwood-controller \
-p 127.0.0.1:8080:8080 -p 2222:2222 \
-v wyrwood-state:/var/lib/wyrwood \
-v ~/wyrwood/controller.yml:/etc/wyrwood/controller.yml:ro \
-v ~/wyrwood/secret-id:/run/secrets/openbao-secret-id:ro \
-v ~/wyrwood/oidc-client-secret:/run/secrets/oidc-client-secret:ro \
ghcr.io/wyrd-company/wyrwood:latest
Print the controller’s SSH host-key fingerprint so you can compare it when an agent logs in:
docker exec wyrwood-controller wyrwood controller host-key
Controller with a local agent
On a Linux Docker host, wyrwood controller serve can run the agent in the
same process. The agent uses the same library, enrollment, approval, policies,
Engine proxy, state, and status commands as a standalone agent. It has no
implicit policy binding or additional OpenBao authority.
Install the Linux package and configure the controller as described above.
Place its document at /etc/wyrwood/controller.yml, with this addition:
local-agent:
enabled: true
The default is false, so the controller container deployment needs no agent
configuration or Docker access. Combined mode needs the wyrwood system
account, its writable state and runtime directories, and the separate
filtering Engine proxy. Use the packaged Linux service on the host for this
mode. The published controller image uses the nonroot account and does not
provide the agent service account or host directories.
Make the controller’s secret files readable by wyrwood. Configure agent
Engine settings in /etc/wyrwood/agent.yml as for a standalone agent. Keep
the proxy separate; do not add the combined process to the Docker group.
Use a service override to run the controller in the packaged agent service:
sudo systemctl edit wyrwood-agent.service
Enter:
[Service]
ExecStart=
ExecStart=/usr/bin/wyrwood controller serve
Start the service with sudo systemctl start wyrwood-agent.service. Its first
start requires a valid agent configuration. To prepare that configuration
before enrollment, set the controller URL in /etc/wyrwood/agent.yml:
controller:
url: ssh://127.0.0.1:2222
With the controller running, enroll the local agent:
sudo wyrwood agent login ssh://127.0.0.1:2222
Compare the displayed fingerprint with wyrwood controller host-key, then
approve the pending key in Agents. The local agent reconnects and appears
under the host’s name. Use the controller’s own reachable SSH address if you
change its listen address or port. Enroll with this controller; the runtime
uses the URL and pinned key in the agent enrollment state.
wyrwood agent status, containers, and events query the embedded agent.
wyrwood agent service start|stop|status manages the combined process through
the existing service. Restart it after configuration changes. Start no separate
agent daemon using the same state; the agent event-store lock rejects a
second runtime. Failure of either service stops both. Missing enrollment or
an unavailable Engine retains the standalone agent’s reconnect behavior.
The consumer root is on the Docker host, so the paths in Container setup
refer directly to that host. Access-group requirements still apply to the
wyrwood account. To return to a standalone agent, stop the service, remove
its override, disable local-agent.enabled, and start the standalone service
and a separate controller deployment with its existing state.
Agent
The agent supports Linux on amd64 and arm64. Install a package from the
Wyrd repository or the Arch User Repository (AUR). Packages create the
wyrwood and wyrwood-engine system users and install the agent and
filtering proxy units. Only the proxy has Docker access. Fresh packages
remain stopped until enrollment. An active agent restarts during upgrades
so that its old Docker group credentials are dropped.
APT
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://repo.wyrd.foo/pubkey.gpg |
sudo tee /etc/apt/keyrings/wyrd-company.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/wyrd-company.gpg] \
https://repo.wyrd.foo/apt stable main" |
sudo tee /etc/apt/sources.list.d/wyrd-company.list >/dev/null
sudo apt update
sudo apt install wyrwood
RPM
sudo curl -fsSL https://repo.wyrd.foo/wyrd.repo \
-o /etc/yum.repos.d/wyrd.repo
sudo dnf install wyrwood
Arch User Repository
wyrwood-bin installs the agent and proxy service files, tmpfiles rules,
and package lifecycle hooks. Its install hook runs the same account and
service setup as the DEB/RPM packages. A fresh installation stays stopped
until enrollment.
Before upgrading an installation that lacks the ALPM hook, complete Prepare an AUR upgrade so a failed service stop aborts the package transaction. Then install or upgrade with your AUR helper:
paru -S wyrwood-bin
Release archive
Each GitHub release provides
Linux tar.gz, DEB, and RPM artifacts for amd64 and arm64, plus
checksums.txt. After extracting the executable, install the service pair and
accounts:
sudo install -m 0755 wyrwood /usr/local/bin/wyrwood
sudo wyrwood agent service install
Configure the access group
An access group is effectively required for non-root containers running as a
UID other than the agent user. Without it, the consumer directory is 0700
and sockets are 0600, owned by the agent user; with it, they are 0750 and
0660. Set the binding’s numeric access group to a host GID held by the
container processes.
Add wyrwood to each consumer access group on the host. Use a group
separate from docker; the agent must not have direct Engine access.
For example, for access group 1000:
getent group 1000
sudo usermod -aG <host-group-name> wyrwood
Replace <host-group-name> with the host group name for GID 1000. Create a
host group with that GID first if needed. Start the agent after enrollment if
it is not running yet. Restart a running agent so it gets the new membership:
sudo systemctl restart wyrwood-agent
Members of this host group who can reach the sockets can use the binding’s OpenBao and SSH permissions. See Access group.
Start the agent
After enrolling the agent, start it:
sudo wyrwood agent service start
This starts the agent immediately and enables it to start at boot for both
package and release archive installations. sudo wyrwood agent service stop
stops the agent without disabling startup at boot. To disable startup at
boot, run sudo systemctl disable wyrwood-agent.
Upgrade order
Use a stopped rollout. Finish active recreations before stopping agents. Resolve an interrupted recreation with the recovery procedure before continuing. Secret and signing socket access is unavailable while an agent is stopped.
-
Stop every agent:
sudo wyrwood agent service stop -
Back up the controller state, then upgrade the controller.
-
Upgrade every agent package or executable while its service is stopped. Package upgrades leave inactive agents stopped.
-
Start the upgraded agents:
sudo wyrwood agent service start
Do not reconnect agents until both peers are upgraded. Agents report the host
consumer directory as consumer-directory; the controller sends the resolved
agent-token-renewal setting. Older peers reject these unknown fields and
close the session. Agents also send binding-status, and a 0.5.x controller
closes the session when it receives one. The controller sends the directory
of a binding that has one, and an agent at 0.6.1 or earlier closes the session
when it receives one: a binding with a directory name needs an upgraded agent.
Neither an agent-first nor a controller-first rolling upgrade keeps mixed
versions connected.
Keep agent keys, policy caches, bindings and consumer directories. The controller moves its database to schema 3 when it starts, in one transaction, and keeps every row. Existing policies keep their OpenBao policy names, existing assignments keep their identity, and live tokens stay valid. Updated agents accept cached assignments without the renewal field and infer their existing choice. Setup examples remain unavailable until the agent reports both the consumer directory and image.
This version moves the database to schema 5 when it starts, in one transaction, and keeps every row. Schema 4 adds the operator keys table, which starts empty. Schema 5 adds a directory name to each binding, and existing bindings have none. The move happens at the first start, also when no binding has a directory name. A 0.5.x controller refuses a schema 4 or 5 database at startup, and a 0.6.1 or earlier controller refuses a schema 5 database; each leaves the file unchanged. Going back needs the backup from step 2.
Back up before an upgrade
Stop the controller and copy its whole state directory, /var/lib/wyrwood in
the controller container. A backup taken before the upgrade is the only way
back.
Go back to an earlier version
An earlier controller refuses a newer database at startup with
database schema is newer than this controller, and leaves the file unchanged.
It also refuses an event log that holds a policy-deleted or policy-renamed
event. To go back, stop the controller and agents, restore the state directory
from the backup, and install the earlier controller and agent versions.
Do not delete the event log to get past a refusal. The log is the audit record, and the database refusal stays.
A backup does not restore OpenBao, and work done after the backup is lost:
- OpenBao policies and AppRoles created after the backup stay in OpenBao. Remove them by hand.
- The controller publishes each restored policy again. A token that outlived its binding, and that names a restored policy, can read through it again until the token expires.
- A restored binding whose AppRole was removed after the backup has no AppRole. It stays marked as applied, so unbind it and bind it again.
- A policy name deleted and reused after the backup can take the same OpenBao policy name again if it is reused after the restore.
Verify
wyrwood version
DEB/RPM and prepared AUR upgrades restart an active agent and its proxy automatically. An inactive agent stays inactive and boot enablement is preserved. For release archive upgrades, replace the executable and run the service installation again:
sudo wyrwood agent service install
Installation removes stored wyrwood membership in docker, preserves its
other consumer groups, and restarts a previously active service with fresh
credentials. If a contract uses the Docker GID as its access group, choose a
separate host group and update the contract before upgrading.
Known generated /etc unit overrides are replaced. Unsafe local units or
drop-ins and remaining processes with Docker access block installation.
This includes a numeric Docker GID or non-empty AmbientCapabilities in
the agent unit.
Correct the reported configuration as root and rerun service install; do
not restore agent Docker access. Keys, policy cache, consumer directories,
and recreate journals are retained.
Consumer sockets are restored from cached policy before the agent contacts the controller, so bound containers keep their endpoints across the restart.
Prepare an AUR upgrade
Pacman does not abort package replacement when an install scriptlet fails.
Wyrwood uses an ALPM PreTransaction hook with AbortOnFail to stop the old
agent before replacing its executable and units. An installation without this
hook needs a root bootstrap before its first upgrade.
Download the target release archive and verify its SHA-256 against that
release’s checksums.txt. Extract it. From the extracted directory, install
the stop script first, then the hook:
sudo install -Dm644 packaging/preinstall.sh \
/etc/wyrwood/engine-upgrade-preinstall.sh
sudo install -Dm644 packaging/wyrwood-engine-bootstrap.hook \
/etc/pacman.d/hooks/wyrwood-engine-upgrade.hook
Then upgrade wyrwood-bin with your AUR helper. The package retains these
files under /usr for subsequent upgrades. The bootstrap uses /etc to
avoid conflicts with incoming package-owned files. A failed stop aborts the
transaction. Correct the error and retry; do not bypass the hook.
Pacman can finish file installation even when post-install service setup
fails. Check its diagnostics and sudo wyrwood agent service status.
Correct any error and run sudo wyrwood agent service install before
starting the agent. A package transaction exit status alone does not prove
that service activation completed.
After successful service installation, remove the bootstrap override so that ALPM uses the package-owned hook and script:
sudo rm /etc/pacman.d/hooks/wyrwood-engine-upgrade.hook \
/etc/wyrwood/engine-upgrade-preinstall.sh
Engine permission
The agent connects to /run/wyrwood-engine/engine.sock. Its proxy directory
is 0750, owned by wyrwood-engine:wyrwood; the socket is 0660. The agent
can connect but cannot replace the listener. The upstream Engine socket keeps
its host permissions. Configuration under /etc/wyrwood is controlled by root.
The proxy allows only the Engine reads the agent needs and container start. The
agent starts a bound container after a host
reboot. A compromised agent can start any
stopped container, but start cannot change a container’s configuration. Setting
engine.allow-recreate: true also permits stop, remove, and create. This opt-in
still permits host takeover if the agent is compromised. Container inspect also
exposes environment values and other host container metadata. Keep recreate
disabled unless that host authority is acceptable.
After a permission or configuration change, activate both processes together:
sudo systemctl restart wyrwood-agent.service
Editing the file alone does not change the active permission. Proxy failures leave Engine observation unavailable; existing consumer sockets continue with their normal token lifecycle. There is no direct Engine fallback.