- Shell 89.6%
- Dockerfile 10.4%
The deploy example was named workflow.yml back when JTAC was deploy-only. Now that the image also serves as the build job container, the examples directory needs a build-side companion — renaming to deploy-workflow.yml + adding build-workflow.yml makes the pairing obvious. build-workflow.yml demonstrates the check-then-build-then-deploy pattern: ask the registry whether the short-SHA tag exists, skip the build if it does (fast-forwards to already-built commits reuse the image), then delegate to deploy-workflow.yml. The DOCKER_HOST quirk (tcp://172.17.0.1:2375 from inside a dind-spawned job) is documented inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| bin | ||
| examples | ||
| .gitignore | ||
| Dockerfile | ||
| README.md | ||
goblin-JTAC
Two small tools for shipping container updates from CI to a target host.
-
goblin-JTAC — a small container image used as a Forgejo Actions job container. As a deploy step it reads SSH config + a deploy key from env (typed as repo secrets), runs one command on the target, exits with the remote's exit code — looks like a test step to the CI pipeline. The image also ships the
dockerCLI so the same image can serve as the build job container in the same pipeline, talking to the runner's dockerd viaDOCKER_HOST. -
goblin-FOB — a target-side bash script that prepares a host to receive JTAC: creates the deploy user, locks down
authorized_keyswith a forced-command wrapper, drops in the example workload script and config templates, and prints the env block to copy into Forgejo secrets.
The default workload is redeploy.sh: pull a tagged image from a
private registry and recreate the rootless quadlet-managed container.
A redeploy-gpass.sh variant defers to gpass site conventions.
Threat model
A JTAC compromise (stolen secrets) lets the attacker call the deploy
endpoint. The endpoint accepts only one command shape
(sudo redeploy.sh <tag>) with a tag matching [A-Za-z0-9._-]{1,128}.
The tag is then looked up in one registry path, configured on the
target — JTAC has no say in which image is pulled. So the attacker
can only deploy images that are already in that registry path —
i.e. images that were built by the same CI pipeline from gated git
history.
The blast radius is "downgrade or jump to a different SHA on the same
image line" — not "deploy attacker code." Combine with a pre-receive
hook on the source forge to prevent untested code from reaching
master, and the chain end-to-end is git-gated.
Layout
bin/
goblin-jtac # container entrypoint (CI-side)
goblin-fob # target-side setup / validation
examples/
redeploy.sh # default workload: raw podman + quadlet
redeploy-gpass.sh # variant for gpass-managed sites
redeploy-wrapper.sh # forced-command wrapper installed by goblin-fob
redeploy.conf.example # non-secret config template (mode 0644)
redeploy.secret.example# pull-token template (mode 0600)
deploy-workflow.yml # Forgejo Actions deploy job (workflow_call)
build-workflow.yml # check-then-build-then-deploy pipeline using
# jtac as the build job container too
Dockerfile # builds the goblin-jtac image
Usage
Build and push the JTAC image
podman build -t registry.goblincorps.com/ci/goblin-jtac:latest .
podman push registry.goblincorps.com/ci/goblin-jtac:latest
Prepare a target host
As root on the target:
sudo bin/goblin-fob setup citopia-ci --workload=gpass --site=citopia
This creates the user, enables linger, drops the wrapper +
redeploy-gpass.sh + config templates into the user's home, and writes
a tightly-scoped /etc/sudoers.d/citopia-ci-redeploy.
Then as the deploy user, fill in the templates and install the deploy public key:
su - citopia-ci
$EDITOR ~/.redeploy.conf ~/.redeploy.secret
goblin-fob install-key < /tmp/jtac-deploy.pub
Finally, capture the env block to paste into Forgejo repo secrets:
sudo -u citopia-ci goblin-fob report
Wire up the Forgejo workflow
Two example workflows:
examples/deploy-workflow.yml— a reusableworkflow_calljob that uses the JTAC image as its job container and provides the captured secrets as env vars. Therun:line is justgoblin-jtac— to the CI pipeline, the deploy step succeeds or fails like any other test step.examples/build-workflow.yml— full check → build → deploy pipeline. Uses JTAC as the build job container too (the image shipsdocker), asks the registry whether the short-SHA tag already exists, skips the build if so, then delegates todeploy-workflow.ymlto ship it. Fast-forwarding a branch to an already-built commit re-uses the existing image instead of rebuilding.
Required env (CI side)
| var | meaning |
|---|---|
JTAC_USER |
SSH user on target (e.g. citopia-ci) |
JTAC_HOST |
SSH host (DNS name or IP) |
JTAC_PORT |
SSH port (default 22) |
JTAC_TAG |
tag/sha to deploy; substituted into JTAC_CMD |
JTAC_DEPLOY_PRIVATE_KEY |
private key, multi-line PEM/OpenSSH |
JTAC_HOST_KEY |
target's SSH server pubkey, ssh-keyscan output |
JTAC_CMD |
optional; default sudo redeploy.sh $TAG |
Required config (target side)
~/.redeploy.conf (mode 0644):
REGISTRY_HOST="registry.goblincorps.com"
IMAGE="citopia/web"
LOCAL_TAG="current"
SVC_USER="citopia" # raw redeploy.sh
QUADLET_UNIT="citopia.service"# raw redeploy.sh
GPASS_SITE="citopia" # redeploy-gpass.sh
~/.redeploy.secret (mode 0600):
REGISTRY_USER="citopia-pull"
REGISTRY_TOKEN="..."