Development¶
Setup¶
git clone https://github.com/ilramdhan/sslsync.git
cd sslsync
make build # bin/sslsync
make test # go vet + go test -race
make lint # golangci-lint (config: .golangci.yml)
Requirements: Go (version in go.mod), golangci-lint v2, Docker for make docker, and Python 3 for the docs.
Docs preview with live reload:
python3 -m venv .venv && .venv/bin/pip install -r docs/requirements.txt
.venv/bin/mkdocs serve # http://127.0.0.1:8000
Layout¶
cmd/sslsync/main.go entry point — only calls app.Main
internal/
app/ commands, flags, .env loading, output; embeds env.example + find-ssl.sh
inventory/ .env → servers and targets; target-type catalogue
scripts/ embedded POSIX sh for Kubernetes (reload / rollback / state / health)
certs/ load + validate the certificate pair; render fullchain/leaf/chain/key/pem
remote/ SSH client: auth, sudo -S, timeouts, host-key algorithms, readable errors
tlsprobe/ read the certificate a host:port serves
runner/ per-server phases, permission fixes, rollback
report/ logs/<run>/run.log, <server>.log, summary.json
scripts/install.sh curl | sh installer (published with the docs)
docs/ this website (MkDocs Material)
.github/workflows/ CI, release, docs, CodeQL, Scorecard
Dependencies only point downwards: app → runner → {remote, tlsprobe, report} → inventory, certs. See Architecture.
Rules of thumb¶
- Errors are for operators: say what is wrong and which variable or command fixes it.
- Shell sent to servers is POSIX
sh. Quote every interpolated value withremote.Shq. It must work on Ubuntu 20.04+. - Anything that changes a server goes through runner phases, so that it is checked first, verified after, and undone on failure.
- Tests generate certificates; never commit real ones. CI rejects
.pem/.key/.crt/.pfx/.envfiles and scans every commit with gitleaks. .env.exampleis embedded in the binary forsslsync init.make build/make testcopy it tointernal/app/env.example, and CI fails if they differ.
Fuzzing¶
Code that reads untrusted input has Go fuzz tests: certificate/key loading (certs.FuzzLoad), .env parsing (inventory.FuzzLoad) and shell quoting (remote.FuzzShq). They check invariants, not just "no panic": rendered PEM is canonical, accepted configuration has valid ports, absolute paths and safe key modes, and sh reads back exactly the string Shq quoted. make fuzz runs each for 30 seconds (make fuzz FUZZTIME=5m for longer); CI runs them on every pull request. A crash is a bug: fix it and keep the input that go test writes to testdata/fuzz/ as a regression seed.
Testing against a real SSH server, safely¶
Never test against production. A disposable container is enough for nginx, MinIO and file targets:
docker run -d --name sslsync-lab -p 2222:22 -p 8443:443 ubuntu:24.04 sleep infinity
docker exec sslsync-lab sh -c 'apt-get update -qq && apt-get install -y -qq openssh-server nginx sudo >/dev/null &&
useradd -m -s /bin/bash deploy && echo deploy:secret | chpasswd && usermod -aG sudo deploy &&
mkdir -p /run/sshd /etc/nginx/ssl && sed -i "s/#PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config &&
/usr/sbin/sshd && nginx'
Put a self-signed "old" certificate in /etc/nginx/ssl/, point an .env at 127.0.0.1:2222, and exercise check, deploy, a failing reload (_NGINX_RELOAD=false, which should roll back), and a rerun (which should report up-to-date).
For Kubernetes, use kind or k3d, or a stand-in kubectl script that stores secrets as files, to test reload, rollback and the state check without a cluster.
Adding a target type¶
- Add an entry to
typesininternal/inventory/targets.go: its files, health checks, reload, and optional rollback/state/vars/keyMode. - Longer shell goes in
internal/inventory/scripts/and is pulled in with//go:embed. - Add cases to
inventory_test.go(valid and invalid configuration). - Document it in Target types and
.env.example.