wyrd/tools
esc

    ↑↓ Move ↵ Open wyrwood results first Search by Pagefind
    Releases

    Policies and contracts

    Policy lives on the controller. Agents receive the policies that match their containers and cache resolved assignments. A controller outage can be tolerated while the current tokens remain valid.

    Controller settings

    Open Settings in the web admin navigation to inspect the controller settings loaded at startup. The page shows listen addresses, session lifetime, state directory, the enabled or disabled local-agent.enabled flag, OpenBao namespace and mounts, operator admission groups, and update check settings. It uses the same operator sign-in requirement as other admin pages. Agent configuration remains on each agent host.

    This view is read-only. Edit the controller YAML configuration and restart the controller to apply changes. Credentials, AppRole RoleID, and credential-file paths are omitted from the page data. Endpoint origins show only scheme and host; URL userinfo, paths, queries, and fragments are omitted.

    Policy document

    name: teal-devcontainer
    openbao:
      agent-token-renewal: true
      policy: |
        path "kv/data/teams/teal/*" { capabilities = ["read"] }
    ssh:
      transit-keys:
        - teal-github
      ca-roles:
        - homelab
    contract:
      mount-target: /run/wyrwood
      env:
        SSH_AUTH_SOCK: "{mount-target}/ssh-agent.sock"
        VAULT_ADDR: "unix://{mount-target}/vault.sock"

    The controller validates the HCL through OpenBao before saving and writes it as wyrwood-teal-devcontainer. Every Transit key and SSH role the policy names must exist under the controller’s configured mounts and must also be permitted by the HCL.

    Policy editor

    The graphical editor adds paths and selects their capabilities. Browse KV paths lists names in the configured KV v2 mounts. Select a secret to add its data/ path, or a directory to add its subtree. Add LIST path for this directory adds a metadata/ grant for the workload. Custom paths accept wildcards and paths outside the browsed mounts. KV names with ACL wildcard or template meaning cannot generate automatic grants; enter their intended scope manually in raw HCL. Graphical path fields direct template syntax to the raw editor.

    Raw HCL exposes the full document. Switching views preserves its text. HCL with constructs the graphical controls cannot represent, including parameter constraints, legacy policy attributes, or inline comments inside a path block, opens as raw HCL. It remains editable and is validated by OpenBao on save. Graphical edits change only the selected path or capability span; other text and comments are retained. Advisory warnings appear beside either view and do not block saving.

    Agent token renewal controls actual renewal of the binding token by the agent. The controller adds fixed lifecycle grants when publishing each named policy: lookup-self uses read, retirement uses revoke-self update, and renew-self uses update when enabled. The graphical editor keeps these paths out of ordinary path editing. Exact lifecycle stanzas in raw HCL are replaced by the managed grants; workload paths and their constraints remain authored. The lifecycle preview shows the resulting grants. New policies enable renewal. For stored documents without openbao.agent-token-renewal, Wyrwood infers the choice from the existing renewal capability, including literal, wildcard, legacy policy attributes, and deny/path priority. An explicit setting overrides those authored lifecycle stanzas. Disabled renewal uses an empty exact rule so that a broader workload path cannot grant it; an enabled selected policy can still contribute update on the same exact path.

    Container lookup forwards the real binding token to OpenBao and returns safe metadata, including its accessor, policies, and remaining TTL. The response omits the token value. Container renew-self is answered locally using lookup metadata and a placeholder credential; it never renews the real token. Container revoke requests are blocked. The agent owns actual renewal and revocation.

    Transit keys and SSH-engine roles have independent offered and not-forwardable selections. Saved names remain visible when an item is missing or OpenBao cannot list it. Add identity name accepts a name that is not in the list, including a forwarding selector for an identity another policy offers. An offered identity must exist in OpenBao.

    Environment requirements use name/value rows. TTL fields use an amount and a unit: minutes or hours on a binding, seconds/minutes/hours for SSH role creation.

    Access group

    The binding form offers groups reported by the running host agent, with numeric GIDs and host names when available. Numeric GIDs are authoritative. The choices include the effective primary group and supplementary groups, plus owner-only access. An agent with effective Linux CAP_CHOWN can also assign another numeric GID. The server rejects groups excluded by the current agent report, including stale form posts.

    For offline or older agents, the form marks capability information unavailable or stale and preserves the existing group as unverified. It does not replace an invalid saved choice. Select a reported group or owner-only access to correct it. Add the host agent user to another group and restart the agent, then reload to see its new report. Account changes alone do not change a running process.

    The container application must retain the selected numeric group as a primary or supplementary group; its primary GID need not match. An entrypoint such as s6-setuidgid can rebuild groups from the container account database and discard groups supplied only through Compose group_add. Check the application process, not only PID 1. The host dropdown does not prove container application access.

    The binding’s settings.access-group is effectively required for non-root containers whose processes run as a different UID from the host agent user. Set it to a numeric host GID that the container processes have as a primary or supplementary group. With an access group, the consumer directory is 0750 and sockets are 0660. Without one, they are 0700 and 0600, owned by the agent user. A container running as another non-root UID cannot use those owner-only sockets.

    The agent user must belong to the host group so it can assign that group to the directory and sockets. Add wyrwood to the consumer access group; docker cannot serve as that group. For example, if the binding uses GID 1000, find its host group name, add the agent user, and restart the agent so it receives the new membership:

    getent group 1000
    sudo usermod -aG <host-group-name> wyrwood
    sudo systemctl restart wyrwood-agent

    Replace <host-group-name> with the name returned by getent. If the GID has no host group, create one with that GID first. Repeat on each bound host. The packaged service identity is wyrwood; the proxy accepts that UID and root.

    Membership grants access: members of that host group who can reach the consumer directory can use its sockets with the binding’s OpenBao and SSH permissions. Choose a group whose members you trust with those permissions.

    Token lifetime

    token-ttl is the initial token lifetime; token-max-ttl, at least as long, is its maximum lifetime. When any selected policy enables Agent token renewal, the agent renews at half the remaining TTL and requests fresh credentials when renewal fails or the maximum lifetime caps a renewal.

    When all selected policies disable renewal, the agent makes no renewal requests. It keeps the token until expiry, stops serving it, and requests a replacement credential from the controller using the existing retry backoff. A successful replacement stops those retries. Retirement cancels the pending expiry and retries. If the controller is unavailable at expiry, secret access stops until a new credential arrives. Container renew-self cannot extend this lifetime. The maximum lifetime bounds a renewing token during a controller outage; the initial TTL bounds a non-renewing token.

    Follow the stopped controller-first rollout. Agents remain stopped until both peers are upgraded. Updated agents accept assignments without the field and infer their existing renewal choice. The controller sends the resolved choice in effective assignments; older agents reject this unknown field. No policy data conversion is required.

    Bindings

    A binding attaches one or more policies to one container key on one agent. Keys are tried in order: devcontainer identity labels, compose project and service labels, container name. Token lifetime, access group, and enforcement are binding settings, independent of the reusable policies:

    {
      "policies": ["teal-devcontainer", "shared-read"],
      "settings": {
        "token-ttl": "1h",
        "token-max-ttl": "8h",
        "access-group": 1000,
        "enforcement": "report"
      }
    }

    The web interface selects policies with checkboxes. Preview effective policy shows source names and revisions, combined HCL, offered identities, forwarding restrictions, contract, and settings before saving. Binding edits submit the current revision; a stale edit must be reloaded.

    Values for each binding

    To give many containers the same rule over different paths, write {{binding.name}} in a path stanza of one policy:

    path "kv/data/sample/keys/{{binding.slug}}" {
      capabilities = ["read"]
    }

    The binding screen then asks for slug, under the selected policies. Each binding holds its own value:

    {
      "policies": ["sample-key"],
      "settings": { "values": { "slug": "alpha" } }
    }

    A required value for each placeholder of the policies

    A token of this binding reads kv/data/sample/keys/alpha and no other key under that path, and Preview effective policy shows the path with the value in it. No policy is written for each container.

    The effective policy shows the path with the value

    A name holds lower-case letters, digits, and underscores, and starts with a letter. A value holds up to 128 letters, digits, underscores, and hyphens, so it cannot add a wildcard or a path segment. Values are not secret: OpenBao holds them in identity metadata, and a token can read its own. Placeholders work only in path stanza names, and saving a policy that has one elsewhere is refused.

    A saved value that no selected policy uses stays on the binding screen as an optional field. It has no effect, and clearing it removes it. Saving a binding with no value for a placeholder is refused. Editing a policy to add a placeholder is refused while a binding that selects the policy has no value, and the message names that binding. To move bindings to a placeholder, create a new policy that has it, select that policy on each binding with its value, and then delete the old policy. Changing a value is a new assignment, so the agent gets a new token and the old token is revoked. No OpenBao policy or controller permission changes are needed.

    How OpenBao combines policies

    HCL remains in separate OpenBao policies. OpenBao evaluates their union with its normal path priority and deny rules; the displayed HCL includes each source in stable name order. Transit keys and SSH roles are deduplicated separately. A not-forwardable selector in any source restricts the selected identity. Sources must require the same mount target. Environment entries are merged after expanding {mount-target}; different values for one name are rejected. A conflicting source edit is rejected before saving.

    The controller creates one AppRole per binding carrying exactly the OpenBao policies of the selected policies, wyrwood-<policy-name> on the first use of a name. It disables the implicit default policy; each named policy contains the fixed and settings-derived lifecycle grants. Renewal is enabled for a composed binding when any selected policy enables it. The effective preview includes the compiled lifecycle grants and the resolved agent-renewal setting. OpenBao still evaluates workload ACL rules, including wildcards and deny precedence.

    Changing the selected policies, a source revision, or binding settings creates a new assignment. The controller updates the AppRole and revokes the reported old token before issuing fresh credentials. An offline agent’s containers lose access until the agent reconnects. A failed transition remains pending and blocks fresh issuance; startup and subsequent credential requests attempt the pending transition. An unreported token remains bounded by its maximum TTL. Late old-assignment accessor reports are revoked rather than saved as current.

    Deleting a binding deletes its AppRole and revokes the reported token even while the agent is offline. If revocation fails, the response says that the token can remain valid until the agent revokes it or it expires.

    Contract enforcement

    report is the default. The agent checks the mount, the environment names and values, and the access group on every reconcile and reports what is missing.

    recreate makes the agent apply the contract once, when the binding is first bound to a container. For a name binding that can be after the binding exists, and the agent then does not need the controller. The agent stops, removes, and creates the container from its inspected definition with the mount and environment merged in. Later drift is reported, not recreated. The host’s agent configuration must also allow recreate; otherwise the binding is reported as non-conforming with that reason.

    Agent configuration

    /etc/wyrwood/agent.yml holds only what is needed to reach the controller and the engine:

    controller:
      url: ssh://controller.example.internal:2222
    engine:
      socket: /var/run/docker.sock
      proxy-socket: /run/wyrwood-engine/engine.sock
      allow-recreate: false

    engine.socket is the proxy’s upstream; the agent uses engine.proxy-socket. The proxy reads the root-controlled file itself. Set allow-recreate: true only when arbitrary create authority on this host is acceptable: a compromised agent can still take over a recreate-enabled host. Activate any change with sudo systemctl restart wyrwood-agent.service, which restarts the proxy too. Both configuration paths and custom proxy socket directories must meet the protected ownership rules described in Installation.

    The agent key, accepted controller fingerprint, cached policy, events, and the journal of each recreate in progress live under /var/lib/wyrwood. No token or secret is ever written there.

    Controller configuration

    /etc/wyrwood/controller.yml names the listen addresses, the state directory, the OpenBao connection, and the OpenID Connect provider operators sign in through. The AppRole secret identifier and the client secret are read from files:

    http:
      listen: 127.0.0.1:8080
      session-ttl: 12h
    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
      transit-mount: transit
      ssh-mount: ssh
      secret-mounts:
        - path: kv
          type: kv-v2
        - path: certificates
          type: other
    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
      groups-claim: groups
      allowed-groups:
        - wyrwood-operators

    openbao.secret-mounts is an optional catalog of expected secret engines, empty by default. Each entry has a unique path inside the namespace, without slashes, and a type of kv-v2 or other. Paths use letters, digits, dot, dash, and underscore; dot-only and double-dot paths are invalid. OpenBao’s reserved roots sys, auth, identity, cubbyhole, and audit cannot be secret mounts. The catalog supports policy warnings and editor path pickers. It creates no mounts and changes no OpenBao permissions.

    Register the controller with the provider as a confidential client using the authorization-code flow with PKCE, with the redirect URL above. The URL is the public address behind your reverse proxy, not the loopback listen address. The provider must include group membership in the ID token, which Wyrwood requests with the groups scope. An account is admitted only when one of its groups is listed under allowed-groups; the web interface cannot change the list.

    Release checks

    The Agents page shows the controller build version and each approved agent’s version. An agent older than the controller is marked Out of date. An agent that omits its version can still connect and shows unknown with Update available. Development builds remain visible; they are not compared as release versions.

    Controller and agent versions with update status

    Release checks are disabled by default. To enable outbound HTTPS requests from the controller to GitHub, add this to controller.yml and restart the controller:

    update-checks:
      enabled: true
      interval: 24h
      timeout: 10s

    interval and timeout are configurable positive Go durations, such as 6h and 20s. There is no additional interval floor or timeout ceiling beyond a positive duration that Go can represent. Choose an interval appropriate for external release checks; very short intervals increase requests to GitHub. The defaults are 24 hours and 10 seconds. An enabled controller checks at startup, then waits the configured interval after each attempt. The timeout bounds each complete check. Checks run in the background, without blocking startup, agent sessions, or pages. Failed checks wait for the same interval; there is no immediate retry.

    The controller reads the public wyrd-company/wyrwood/releases/latest redirect using an HTTP client, without the GitHub API or a credential. It compares the release tag with its build version using SemVer, including prereleases and build metadata. The page shows Update available with the release link when a newer release exists. Up to date requires a successful check and a comparable build version.

    For an offline or air-gapped controller, leave enabled false or omit update-checks. No release request is made. If an enabled check fails, the page shows Update check unavailable, writes a fixed failure category to the controller journal, and retains the last successful result with its age. Observations are held in memory and start empty after a restart.

    Policy path warnings

    The policy editor checks HCL path stanzas against the configured AppRole, Transit, SSH, and secret-engine mounts, plus the exact auth/token/lookup-self, auth/token/renew-self, and auth/token/revoke-self paths. A path such as kv/* or kv/data/+/entry stays inside a configured kv mount; kv*, +/data/*, auth/+/role/*, and sys/* can reach outside the configured mounts and produce an advisory warning. The check reads path stanzas, including deny stanzas; it does not evaluate effective OpenBao capabilities or path priority.

    Checks run when the editor opens and when you leave the HCL field. A warning, unreadable HCL, or an unavailable warning check never blocks saving. OpenBao validates the HCL on save. An outside grant can be intentional; list a secret engine in openbao.secret-mounts when it is part of the expected scope.

    OpenBao namespace

    The controller runs in a dedicated OpenBao namespace. OpenBao does not limit the HCL a token writes to the capabilities that token holds, so the controller, and any operator, can grant every access in the namespace the controller runs in. A dedicated namespace keeps that access inside Wyrwood.

    The namespace holds:

    • the AppRole auth mount the controller and binding AppRoles use
    • the controller’s own policy and AppRole
    • the Transit and SSH-engine mounts the controller manages
    • every secret engine containers read, for example KV

    Paths in policies are relative to the namespace, so a policy names kv/data/..., not wyrwood/kv/data/.... Set openbao.namespace to the namespace path; the controller, the agents, and the container vault sockets all send it.

    Name the controller’s own policy and AppRole outside the wyrwood- prefix, for example controller. The controller writes only wyrwood- names, so it cannot rewrite its own policy or AppRole. Every binding AppRole carries only its selected wyrwood- policies and no implicit default policy.

    The controller’s policy, for a kv-v2 secret-mount entry at kv and the default approle, transit, and ssh mounts:

    path "sys/policies/acl/wyrwood-*" {
      capabilities = ["create", "read", "update", "delete", "list"]
    }
    path "auth/approle/role/wyrwood-*" {
      capabilities = ["create", "read", "update", "delete", "list"]
    }
    path "auth/approle/role/wyrwood-*/role-id" { capabilities = ["read"] }
    path "auth/approle/role/wyrwood-*/secret-id" {
      capabilities = ["create", "update"]
    }
    path "sys/wrapping/wrap" { capabilities = ["update"] }
    path "kv/metadata" { capabilities = ["list"] }
    path "kv/metadata/*" { capabilities = ["list"] }
    path "transit/keys" { capabilities = ["list"] }
    path "transit/keys/*" { capabilities = ["create", "read", "update", "list"] }
    path "ssh/roles" { capabilities = ["list"] }
    path "ssh/roles/*" { capabilities = ["create", "read", "update", "list"] }
    path "auth/token/renew-self" { capabilities = ["update"] }
    path "auth/token/revoke-accessor" { capabilities = ["update"] }

    Change the mount names in the paths when you configure other mounts. The controller revokes only the token accessors its agents report.

    The editor browses only kv-v2 entries from openbao.secret-mounts. The list is empty by default; free-text paths and raw HCL remain available. Browsing requires a non-root namespace. Grant only list on each configured KV v2 mount’s metadata and metadata/* paths in that namespace, as shown for kv above. Entries of type other do not get KV LIST grants or appear in the picker. The controller does not read KV metadata or data values, write KV data, or list sys/mounts for the editor. A denied or unavailable list shows a warning without blocking manual editing.

    Controller local agent

    Set local-agent.enabled: true in the controller document to run the shared agent runtime inside controller serve. It defaults to false. Agent settings remain in /etc/wyrwood/agent.yml; enrollment and local commands retain the standard agent state and runtime paths. Follow Controller with a local agent for the Linux service setup and approval steps.