Files
monky-deployd/docs/OPERATIONS.md
T
mdella 1c42e913a8 feat: monky-deployd v0.1.0 — pull agent over the mesh (ADR-0028)
Stdlib-only Python 3.12 agent for docker VMs and laptops: flock → checkin
(bearer = the agent's OpenBao token, bootstrapped from the install kit's
jwt-tenancy deploy grant) → action apply|none|down → bundle (sha256
verified) → refusal checks (unresolved ${VAR} names only, manifest paths
pinned to monky/data/<env>/see/, privileged/host-network, rollback, disk
need×1.5+headroom) → lease → POST /v1/auth/jwt-tenancy/login → KV reads →
.env 0600 → promote → compose pull/up → wait healthy → report; finally
renew-self / re-lease before max TTL, scrub. Exit 0/75/78/1. Redactor log
filter. Transports sdk (openziti) / proxy (ziti tunnel proxy 18443/18200) /
system. Laptop mode.

Packaging: hardened oneshot + 60 s timer + proxy unit, nfpm .deb with
/opt/monky-deployd/venv, install.sh for Ubuntu 26.04 (Gitea release
download, enrol, ACLs, bootstrap from stdin), ansible role skeleton for
osg1-07. CI: lint/test on every change; wheel (openziti on ubuntu:26.04) and
package (nfpm) allow_failure until runner egress is proven; GitLab release +
release:gitea on v* tags. Docs: README, PROTOCOL, OPERATIONS, CHANGELOG,
CLAUDE/AGENTS.

Divergence noted: monky-tenancy main (MR !15) still ships the AppRole lease
and kit; this agent implements the plan's Gate 1 RESULT (login_jwt, no
unwrap) and refuses an AppRole lease loudly (LEASE_SHAPE).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
2026-09-05 08:01:36 +00:00

6.7 KiB

Operating monky-deployd

Units

unit what
monky-deployd.timer fires monky-deployd.service 60 s after the previous tick finished (+ ≤10 s jitter); OnBootSec=90s
monky-deployd.service Type=oneshot, monky-deployd run --once as user monky-deployd (+ docker group); SuccessExitStatus=75; hardened (NoNewPrivileges, ProtectSystem=strict, ReadWritePaths=/var/lib/monky-deployd /etc/monky-deployd /run/docker.sock, UMask=0077, no capabilities)
monky-deployd-proxy.service only with transport: proxy: ziti tunnel proxy -i <identity> monky.tenancy.deploy:18443 openbao:18200 as user ziti; EnvironmentFile=/etc/monky-deployd/proxy.env
ziti-edge-tunnel.service the host identity's tunneler in run-host mode (drop-in run-host.conf written by install.sh)
systemctl status monky-deployd.timer monky-deployd.service
systemctl list-timers monky-deployd.timer
systemctl start monky-deployd.service          # tick now (blocks until done)
journalctl -u monky-deployd -f                 # priorities are real (INFO/WARNING/ERROR)
monky-deployd status                           # exit 0 = token present and in sync
monky-deployd status --json | jq .

Files: /etc/monky-deployd/config.yaml (0640 root:monky-deployd), /etc/monky-deployd/openbao-ca.pem, /etc/monky-deployd/bootstrap.jwt (only until the first login), /var/lib/monky-deployd/{bao.token,state.json,lock}, /var/lib/monky-deployd/<env>/releases/<sha>/ + current (the compose project dir, .env 0600), /opt/openziti/etc/identities/monky-host.<env>.json (ziti:ziti 0600 + ACL u:monky-deployd:r).

Reading the journal

line meaning
checkin: action=none desired=… applied=… then healthy; heartbeat reported converged
checkin: action=apply …read 3 secret(s): GEMINI_API_KEY, …promoted release …applied … a deploy
refused: ENV_INCOMPLETE: unresolved: X the bundle needs a variable no manifest entry supplies — fix the descriptor / set the secret in the console; nothing was started
refused: DISK_INSUFFICIENT: docker data-root has N MiB free, bundle needs M MiB free space (the env-dev-09 lesson): grow the data-root disk or prune
refused: PRIVILEGED_REFUSED / ROLLBACK_REFUSED the bundle needs allow_privileged / allow_rollback in its agent profile
temporary network failure (exit 75) mesh/tenancy unreachable — check ziti-edge-tunnel, the identity's terminators, monky.tenancy.deploy health
AGENT_UNAUTHENTICATED: bearer refused (exit 1) the grant was superseded (kit re-revealed / retire) or the token revoked → re-run the install kit
AGENT_ENV_MISMATCH (exit 78) the token belongs to another env than config.yaml — fix the config or re-issue the identity; the timer keeps firing but every tick exits 78 immediately (no storm)
failed: docker compose pull failed (rc=1) registry/pull problem; compose output is in the report's tail and in the journal

Values never appear in the journal ([REDACTED] for token shapes and every value the agent has read).

Secrets on the box

The only credential is /var/lib/monky-deployd/bao.token (0600, user monky-deployd). .env files under releases/<sha>/ hold the rendered values (0600, same user, docker compose reads them). Rotating a secret in the console changes the bundle sha → the next tick re-reads and re-applies. To force a re-read now: systemctl start monky-deployd.service.

Lost or revoked token: monky-deployd status shows token ABSENT; reveal the kit again (admin, GET /v1/backends/{id}/agent/install), paste its bootstrap grant to /etc/monky-deployd/bootstrap.jwt (0600 monky-deployd) or re-run install.sh (enrolment is skipped when the identity exists), then monky-deployd bootstrap or wait a tick.

Retire and volumes policy

Retire is driven by tenancy (POST /v1/backends/{id}/retire {confirm, force, purge_volumes}): the next check-in returns action: down and the agent runs docker compose down --remove-orphans. Data volumes:

  • -v (remove volumes) only when tenancy sent purge_volumes: true or the box's volumes_on_absent: purge — and never on a prod env (the request is logged and ignored).
  • default volumes_on_absent: keep: volumes stay for a manual docker volume rm later.

After down the agent reports down, clears applied_sha, removes current and keeps releases/ (no secrets outside .env, which you can shred). Tenancy then destroys the Bao paths, revokes the token accessor and deletes the host identity → the following ticks fail with AGENT_UNAUTHENTICATED; systemctl disable --now monky-deployd.timer and apt remove monky-deployd (apt purge also removes /var/lib/monky-deployd and /etc/monky-deployd).

Laptop mode

laptop_mode: true (install.sh --laptop): being offline is normal — a tick that cannot reach the mesh logs one INFO line and exits 0. Run without the system timer:

monky-deployd run             # loop, interval_s + jitter, SIGTERM stops; or a user timer with `run --once`

Liveness grace for laptops is 24 h on the tenancy side (P0); offline only hides the backend from the picker, nothing is retired automatically.

Housekeeping

monky-deployd run --once --prune removes every releases/<sha> except current and runs docker image prune -f after a successful apply. The state dir and the token survive apt remove; apt purge deletes them.

Publishing a release to Gitea by hand

When release:gitea is manual (no GITEA_TOKEN in CI) — from jump1 with the Gitea API token:

T=$(tr -d '\n' < ~/.gitlab_tokens/gitea-token); G=http://172.16.8.1:3000/api/v1/repos/mdella/monky-deployd
curl -s -X POST -H "Authorization: token $T" $G/mirror-sync                      # pull the tag
RID=$(curl -s -X POST -H "Authorization: token $T" -H 'Content-Type: application/json' $G/releases \
      -d '{"tag_name":"v0.1.0","name":"monky-deployd v0.1.0","body":"See CHANGELOG.md"}' | jq -r .id)
for f in monky-deployd_0.1.0_amd64.deb monky-deployd_0.1.0_amd64.deb.sha256 install.sh; do
  curl -s -X POST -H "Authorization: token $T" -F "attachment=@$f" "$G/releases/$RID/assets?name=$f"; done

The assets are then at https://gitea.cbs.tikali.net/mdella/monky-deployd/releases/download/v0.1.0/<file>, which is what install.sh fetches (GitLab artifacts from the tag pipeline's package job).

Ansible (osg1-07)

ansible/roles/monky_deployd/ is the role skeleton to copy into osg1-07: .deb from the Gitea release (sha256-verified), config template, bootstrap grant from a vault var, ACL on the identity, timer, one tick. Rolled to env-dev-06..09 after the env-qa-02 pilot; env-dev-01 last.