A GitHub Action and supporting Docker stack for signing build artefacts and git tags using a Sigul signing server. Two deliverables live in this repository:
- A composite GitHub Action (
sigul-docker) that workflows can use to sign files or git tags by talking to a Sigul server over its bridge. - A complete reference Sigul stack — server + bridge + client containers, Docker Compose definition, deployment scripts and end-to-end test suite — used to validate the action against a live Sigul instance and to provide a reproducible local debugging environment.
The action is a GitHub composite action that builds the Sigul client image
on the runner and uses it to sign one or more workspace files or a git
tag. The build only reuses an already-present local image, so on
GitHub-hosted runners (where the Docker daemon is per-job) the build
runs once per job; on self-hosted runners the image persists across
jobs that share the same daemon. If you need cross-run caching, run a
docker/build-push-action step with cache-from / cache-to: type=gha
and tag the loaded image as either
client-${PLATFORM_ID}-image:${PLATFORM_ID} or
client-${PLATFORM_ID}-image:action ahead of the uses: line below —
those are the tags this action's build step looks for before deciding
to skip its own docker build.
- uses: lfreleng-actions/sigul-sign-docker@v1
with:
sign-type: 'sign-data'
sign-object: ${{ github.workspace }}/artifacts/mypackage.tar.gz
sigul-key-name: 'my-release-key'
sigul-conf: ${{ secrets.SIGUL_CONF }}
sigul-pass: ${{ secrets.SIGUL_PASS }}
sigul-pki: ${{ secrets.SIGUL_PKI }}
# Produces ${sign-object}.asc next to the input file.
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Signatures
path: ${{ github.workspace }}/artifacts/mypackage.tar.gz.asc- uses: lfreleng-actions/sigul-sign-docker@v1
with:
sign-type: 'sign-data'
sign-object: |
file.tar.gz
artifacts/my-file.jar
docs/signme.md
sigul-key-name: 'my-release-key'
sigul-conf: ${{ secrets.SIGUL_CONF }}
sigul-pass: ${{ secrets.SIGUL_PASS }}
sigul-pki: ${{ secrets.SIGUL_PKI }}
# The action preserves directory structure: dir/sub/file.ext is signed in
# place as dir/sub/file.ext.asc.- uses: lfreleng-actions/sigul-sign-docker@v1
with:
sign-type: 'sign-git-tag'
sign-object: 'v1.1' # Existing unsigned annotated tag in the repo
sigul-key-name: 'my-release-key'
gh-user: automation-username
gh-key: ${{ secrets.GITHUB_TOKEN }}
sigul-conf: ${{ secrets.SIGUL_CONF }}
sigul-pass: ${{ secrets.SIGUL_PASS }}
sigul-pki: ${{ secrets.SIGUL_PKI }}| Input | Required | Default | Description |
|---|---|---|---|
sign-type |
no | sign-data |
Either sign-data or sign-git-tag. |
sign-object |
yes | — | File to sign (or newline-separated list of files), or the name of an annotated git tag. |
sigul-key-name |
yes | — | Name of the key on the Sigul server to sign with. |
sigul-conf |
yes | — | Body of the Sigul client configuration file. The action's entrypoint writes it to client.conf inside the container's Sigul config directory (/var/sigul/config, falling back to /etc/sigul or $HOME/.sigul-config) and passes the resulting path to every sigul --batch -c … invocation. The bridge hostname / port / NSS settings the client needs all live here. |
sigul-pass |
yes | — | Passphrase for the Sigul key. Also used as the GPG passphrase to decrypt sigul-pki. |
sigul-pki |
yes | — | Client PKI material: a tar.xz archive containing a .sigul/ directory (NSS database, certificates, private key), GPG-encrypted with sigul-pass. May be supplied raw or base64-encoded; the entrypoint auto-detects. |
gh-user |
no | github.actor |
GitHub user to push the signed tag as (sign-git-tag only). |
gh-key |
no | — | GitHub API key for gh-user. Required for sign-git-tag; ignored for sign-data. |
sigul-mock-mode |
no | false |
When true, emit deterministic mock signatures locally without contacting a Sigul server. Useful for testing workflow plumbing. |
To use the action against a real signing server you need:
- A reachable Sigul server with bridge.
- A Sigul key whose passphrase matches
sigul-pass. - A
client.confbody forsigul-confthat points the client at the right bridge (typicallybridge-hostnameand the matching bridge cert nickname in[nss]). - A
sigul-pkiarchive whose certificates align with the keys and hostnames the bridge expects. - Network connectivity from the GitHub Actions runner to the Sigul bridge.
This repository builds three containers, all from fedora:44:
| Container | Dockerfile | Role |
|---|---|---|
| Client | Dockerfile.client |
Sigul client used by the action and the integration tests. |
| Server | Dockerfile.server |
Sigul server: holds the signing keys, runs SQLite-backed key/user database. |
| Bridge | Dockerfile.bridge |
Sigul bridge: brokers double-TLS connections between clients and the server. |
All three images install Sigul from the upstream source tree at
pagure.io/sigul by way of
build-scripts/install-sigul.sh. The
same script is used for both linux/amd64 and linux/arm64; there is no
architecture-specific install logic. Pinned local fixes live in
patches/ and apply in numeric order during the image build.
python-nss-ng (the Python bindings) is installed from PyPI rather than
distro packages. SQLite is the only database used by the server; there is no
PostgreSQL dependency.
client --(TLS, port 44334)--> bridge --(TLS, port 44333)--> server
bridge-hostname(default:sigul-bridge) — hostname clients and servers use to reach the bridge.client-listen-port(default:44334) — bridge port for client connections.server-listen-port(default:44333) — bridge port for server connections.- The Sigul bridge unconditionally binds to
0.0.0.0(all interfaces); access control is the responsibility of the surrounding container network or firewall configuration.
linux/amd64linux/arm64
Both architectures are built and tested on every PR; the same test suite runs against each.
The Sigul Build/Test 🐳 workflow defined in
.github/workflows/build-test.yaml
builds the three images for both architectures, runs the integration and
signing test suites against the resulting stack, and (on workflow_dispatch
with publish_ghcr: true) publishes the images to GHCR.
Workflow trigger: pull_request to main, plus workflow_dispatch with the
following inputs:
| Input | Default | Purpose |
|---|---|---|
clear_cache |
false |
Bypass GitHub Actions cache and rebuild images from scratch. |
enable_auth_debug |
false |
Set SIGUL_DEBUG_AUTH=1 in the bridge and server, surfacing AUTHDBG/* lines in container logs. |
publish_ghcr |
true |
Publish freshly-built images to ghcr.io/<org>/<repo>/sigul-docker. Untick when iterating on the workflow itself. |
- Docker Engine or Docker Desktop with Compose V2.
bash,git.- A few hundred MiB of free disk space for the three images.
# 1. Build the three images for your host architecture.
# Replace 'linux-arm64' with 'linux-amd64' on Intel hosts.
PLATFORM_ID=linux-arm64
for component in client server bridge; do
docker build \
--platform "linux/${PLATFORM_ID#linux-}" \
-f "Dockerfile.${component}" \
-t "${component}-${PLATFORM_ID}-image:test" \
.
done
# 2. Deploy the stack (server + bridge + cert-init).
SIGUL_RUNNER_PLATFORM=${PLATFORM_ID} ./scripts/deploy-sigul-infrastructure.shThe deploy script writes an ephemeral admin password to
test-artifacts/admin-password and an NSS database password to
test-artifacts/nss-password; the test scripts read both back from disk.
The repository ships two end-to-end suites, both of which CI runs against the live stack:
# Control-plane tests: list-users, list-keys, double-TLS handshake, etc.
SIGUL_CLIENT_IMAGE=client-${PLATFORM_ID}-image:test \
./scripts/run-integration-tests.sh
# Full signing workflow: key lifecycle, sign-text / sign-data / sign-rpm /
# sign-rpms, user and key-access lifecycle. Each output is independently
# verified with the upstream tool that would consume it (gpg, rpm).
SIGUL_CLIENT_IMAGE=client-${PLATFORM_ID}-image:test \
./scripts/run-signing-tests.shrun-signing-tests.sh writes its scratch state to a mktemp directory and
cleans up via a trap on EXIT; nothing is left behind on a successful run.
# Removes all containers, networks and named volumes Compose created
# from this docker-compose.sigul.yml regardless of project prefix.
docker compose -f docker-compose.sigul.yml down -v --remove-orphansDEPLOYMENT_GUIDE.md— deploying the stack outside CI.OPERATIONS_GUIDE.md— day-to-day operation, monitoring, health checks.TESTING.md— test infrastructure overview.patches/README.md— what each downstream Sigul patch fixes and why.docs/— deeper dives on individual topics.
- TLS / NSS / handshake errors (
Unexpected EOF in NSPR, silent timeouts, opaque auth failures): setSIGUL_DEBUG_AUTH=1locally, orenable_auth_debug: trueon theworkflow_dispatchform in CI, to surfaceAUTHDBG/*lines in the bridge / server logs (added by patch 02). Then run./scripts/debug-tls-stack.sh --all --verbose. Seedocs/TLS_DEBUGGING_GUIDE.mdanddocs/DEBUGGING_QUICK_REFERENCE.mdfor the full walk-through. - Stack won't come up cleanly: run the
scripts/validate-{volumes,nss,certificates,configs}.shhelpers; each supports--helpand is safe to run in any order. - Capturing a bundle to attach to an issue:
./scripts/collect-sigul-diagnostics.sh --compressproduces a redacted tarball underdiagnostics/. - Last resort: stale volumes are the most common cause of
"impossible" stack bugs.
docker compose -f docker-compose.sigul.yml down -v --remove-orphansand redeploy.
Contributions land through GitHub pull requests against main. The
Sigul Build/Test 🐳 workflow is required to pass before a PR can
merge — it builds all three images for both linux/amd64 and
linux/arm64, runs the integration test suite, and then runs the full
end-to-end signing test suite against the resulting stack. Treat a
failing CI run as the source of truth.
- Sigul behaviour fixes — add a numbered patch to
patches/(NN-short-description.patch). The patch applies on top of the bundled Sigul source during the image build. Document every patch inpatches/README.mdusing the sameStatus / Affects / Problem / Fix / Impactstructure as the existing entries; if a patch is critical for the stack to start at all, mark it as such. Verifygit apply --checkworks against the bundled Sigul source tree before pushing. Note that upstream Sigul on Pagure has not had a commit in over a year and Pagure itself is scheduled to be decommissioned around mid-2026, so in practice these patches are a permanent local fork rather than a staging area for upstream submission. - Container build / packaging changes — prefer
build-scripts/install-sigul.shover editing the Dockerfiles, so the install path stays uniform acrosslinux/amd64andlinux/arm64. When you do touch a Dockerfile, change all three (Dockerfile.{client,server,bridge}) consistently — they share a base image and most of their package set. - Test changes — the two end-to-end suites are
scripts/run-integration-tests.sh(control plane: list-users, list-keys, double-TLS handshake, etc.) andscripts/run-signing-tests.sh(key lifecycle, sign-text/data/rpm/rpms, user and key-access lifecycle, with each output independently verified by gpg or rpm). New tests should fit into the existingphase/testcase/pass/failshape and remain idempotent against repeated runs. - Workflow / CI changes —
build-test.yamlis the only workflow that exercises the stack end-to-end; iterate on it viaworkflow_dispatchwithpublish_ghcr: falseuntil it's green. - Documentation changes — keep the assertions in this README,
DEPLOYMENT_GUIDE.md,OPERATIONS_GUIDE.mdandTESTING.mdconsistent with what the scripts and Dockerfiles actually do. When you delete a script or rename a file, run agrep -rnfor the old name across the repo and update or drop the dangling references.
- Build the three images for your host architecture (see Bringing up the stack locally).
- Bring the stack up with
scripts/deploy-sigul-infrastructure.sh. - Run
scripts/run-integration-tests.shandscripts/run-signing-tests.sh; both should exit0with all tests passing. - If you changed the action surface, run a manual
workflow_dispatchofSigul Build/Test 🐳withpublish_ghcr: falseto confirm bothlinux/amd64andlinux/arm64legs stay green.
- Conventional Commits, capitalised types:
Fix(scope):,Feat(scope):,Docs(scope):,Refactor(scope):,Test(scope):,Chore(scope):,CI(scope):,Build(scope):,Perf(scope):,Style(scope):,Revert(scope):. See.gitlintfor the enforced set. - Subject ≤ 50 chars, body wrapped at 72 (URL lines exempt).
- DCO sign-off required — every commit must end with
Signed-off-by: Name <email>; usegit commit -s. - Atomic commits — one logical change per commit. In particular, do not mix code or doc changes with task-tracking updates.
- Pre-commit hooks — the repository ships a
.pre-commit-config.yamlthat runs ruff, mypy, yamllint, actionlint, reuse (SPDX), codespell, markdownlint, gitlint and a few project-specific validators. Install withpre-commit install; never bypass with--no-verify. If a hook auto-fixes files, stage the fixes and re-commit — do notgit resetafter a failed commit. - AI-assisted commits — include a
Co-authored-by:trailer for the model used (e.g.Co-authored-by: Claude <claude@anthropic.com>) immediately above theSigned-off-byline. - SPDX headers — every new source file needs SPDX
Apache-2.0and copyright headers. SeeREUSE.tomlfor file-type-specific patterns; thereusepre-commit hook will flag misses.
Use GitHub Issues to report problems and bugs.
A good report includes the steps to reproduce, what you expected to
happen versus what actually happened, the relevant container or
workflow logs, and — for stack-level bugs — the diagnostic bundle
from scripts/collect-sigul-diagnostics.sh --compress (see
Troubleshooting).