This document is LLM-generated.
Optional tag-triggered deployment: pushing a prod-* tag makes CI build a
runtime-only tarball (main.py, piclock/, requirements files) and pipe it over
SSH to the Pi, where a locked-down account applies it. Nothing here runs
as-is — deploy.yml.example is a template, and the host
side needs a one-time bootstrap.
Security model: CI holds the key for piclock-deploy, an unprivileged account
whose key is pinned to a single forced command, piclock-receive.
That script validates the request, stages the tarball, and sudos exactly one
root command, piclock-apply, which syncs /opt/piclock,
refreshes the clocker venv, and try-restarts the service. It never touches
units, accounts, sudoers, or these scripts — so a leaked deploy key can only
ship code that runs as the unprivileged clocker, never escalate to root.
Runtime copying and dependency installation run as clocker, including when
invoked by the root helper. Staging is readable by that account; receive/apply
locks serialize overlapping deployments. The deploy home is root-owned so the
account cannot replace its .ssh directory. The account's authorized_keys
is also root-owned (so a hostile payload can't rewrite its own forced-command restriction — see
bootstrap step 4), and CI pins the Pi's host key (so the tarball and trigger
can't be intercepted by whatever answers to the Pi's name).
-
Run
sudo scripts/install.shfirst — the service, theclockeraccount, and/opt/piclockmust already exist. -
Create the deploy account (a real shell is required for the forced command):
sudo useradd --system -m -d /var/lib/piclock-deploy -s /bin/sh piclock-deploy sudo chown root:root /var/lib/piclock-deploy sudo chmod 755 /var/lib/piclock-deploy sudo install -d -o piclock-deploy -g piclock-deploy -m 0755 /var/lib/piclock-deploy/staging sudo touch /var/lib/piclock-deploy/receive.lock sudo chown piclock-deploy:piclock-deploy /var/lib/piclock-deploy/receive.lock
-
Install both scripts root-owned:
sudo install -o root -g root -m 0755 deploy/piclock-receive deploy/piclock-apply /usr/local/sbin/
-
Generate a keypair (
ssh-keygen -t ed25519 -f piclock-deploy-key -N '') and pin the public key in/var/lib/piclock-deploy/.ssh/authorized_keys, with this one line as its contents:command="/usr/local/sbin/piclock-receive",restrict ssh-ed25519 AAAA... piclock-ciMake the directory and file root-owned and read-only to the account (sshd's
StrictModesaccepts root ownership):sudo install -d -o root -g root -m 0755 /var/lib/piclock-deploy/.ssh sudo install -o root -g root -m 0644 authorized_keys /var/lib/piclock-deploy/.ssh/
Root ownership is deliberate, not just tidiness:
piclock-receiveextracts a CI-supplied tarball aspiclock-deploy, so the account itself must not be able to edit its own key restrictions — otherwise a malicious payload that escaped the staging directory could rewriteauthorized_keysand remove the forced-command pinning. -
Allow the one privileged step, in
/etc/sudoers.d/piclock-deploy(mode 0440):piclock-deploy ALL=(root) NOPASSWD: /usr/local/sbin/piclock-apply
- Copy deploy.yml.example to
.gitea/workflows/deploy.ymland replace<pi-host>with your Pi's hostname or IP. - Add two repository secrets:
PICLOCK_DEPLOY_KEY— the private key from step 4 above.PICLOCK_HOST_KEY— the Pi's public host key, so CI can verify it is talking to the real Pi. Capture it once withssh-keyscan -t ed25519 <pi-host>(run from a machine that you trust is really reaching the Pi; use the same hostname/IP as in the workflow) and paste the output line in verbatim. If you ever reimage the Pi, re-run the scan and update the secret.
- Deploy by tagging:
git tag prod-2026-01-01.1 && git push origin prod-2026-01-01.1.
Note the workflow deploys only the runtime bundle — it will not create accounts,
change systemd units, or apply new feature flags. For those, run
scripts/install.sh on the Pi as before.
These scripts are host configuration and are not included in runtime deploys.
When upgrading them, apply the ownership, staging-directory and lock-file setup
in step 2, then reinstall both scripts as in step 3. The staging tree must be
readable by clocker; the receiver normalizes payload permissions. HomeKit
requirements are refreshed when the installer-managed HomeKit drop-in exists.
Deployments update the runtime in place and have no automatic rollback. A failed
dependency install can leave a partially updated bundle. Custom HomeKit enablement
outside the installer-managed homekit.conf needs its own dependency refresh.