When you're ready to deploy, JTAC has eyes on target.
  • Shell 89.6%
  • Dockerfile 10.4%
Find a file
John McCardle fee6d27176 Add build-workflow example, rename deploy example for symmetry
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>
2026-05-19 02:26:07 +00:00
bin initial scaffold: goblin-JTAC + goblin-FOB 2026-05-04 02:40:56 +00:00
examples Add build-workflow example, rename deploy example for symmetry 2026-05-19 02:26:07 +00:00
.gitignore initial scaffold: goblin-JTAC + goblin-FOB 2026-05-04 02:40:56 +00:00
Dockerfile Add docker CLI so jtac can serve as the build job container too 2026-05-19 02:24:36 +00:00
README.md Add build-workflow example, rename deploy example for symmetry 2026-05-19 02:26:07 +00:00

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 docker CLI so the same image can serve as the build job container in the same pipeline, talking to the runner's dockerd via DOCKER_HOST.

  • goblin-FOB — a target-side bash script that prepares a host to receive JTAC: creates the deploy user, locks down authorized_keys with 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 reusable workflow_call job that uses the JTAC image as its job container and provides the captured secrets as env vars. The run: line is just goblin-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 ships docker), asks the registry whether the short-SHA tag already exists, skips the build if so, then delegates to deploy-workflow.yml to 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="..."