This guide explains how to build a container image for the Signum Agent from a released Linux package.
To build your own Signum Agent Container, take the Agent package (.deb or .rpm) supplied by Keyfactor, unpack it into a minimal base OS, install the .NET runtime and PKCS#11 dependencies, and run the service. No source compilation is involved.
Prerequisites
-
Docker (or Podman) with BuildKit enabled
-
The Signum Agent package for your target OS and CPU architecture, obtained
from Keyfactor:-
Ubuntu 22.04:
signum-agent-<arch>.deb -
AlmaLinux 9 / RHEL 9:
signum-agent-<arch>.rpm -
<arch>isamd64/x86_64for Intel/AMD orarm64/aarch64for ARM.
-
Place the package and the entrypoint script next to the Dockerfile before building.
Supported Base Images
|
Family |
Recommended base image |
Package format |
|---|---|---|
|
Debian/Ubuntu |
|
|
|
RHEL-compatible |
|
|
What the Package Installs
Both the .deb and the .rpm place their files in the same locations (the
Dockerfiles unpack the package rather than installing it — see the build steps
below for why):
|
Path |
Contents |
|---|---|
|
|
|
|
|
|
|
|
PKCS#11 driver |
|
|
Agent config ( |
The Dockerfiles below add two more files that the package does not ship but
that signing tools need to locate the token:
|
Path |
Purpose |
|---|---|
|
|
PKCS#11 provider config referenced by Java tools ( |
|
|
Registers the driver with p11-kit so p11-kit-aware tools (OpenSSL, |
Runtime Dependencies
The final image needs the .NET 10 runtime plus a few libraries:
|
Purpose |
Ubuntu 22.04 package |
AlmaLinux 9 package |
|---|---|---|
|
.NET runtime |
|
|
|
PKCS#11 / smartcard libs |
|
|
|
Secure credential storage |
|
|
|
Startup script helpers |
|
|
If you use a standalone (self-contained) package, the .NET runtime is
bundled inside the agent. In that case, omit the aspnetcore-runtime-10.0 and
dotnet-runtime-10.0 packages.
Set up the Environment
ENTRYPOINT
Alongside your Dockerfile, you need a start.sh ENTRYPOINT.
When the container starts, the start.sh performs the following:
-
Reads the
SIGNUM_*environment variables (see Running). -
Writes the service configuration to
/etc/keyfactor/configand the agent
identity to/etc/keyfactor/agentIds. -
Launches
SignumServicein the background. -
Registers the agent by calling
signum-util setup(Signum backend) or
signum-util dss(SignServer backend), then waits on the service.
This is the start.sh bundled on the official signum-agent container from release 4.80.2:
#!/bin/bash
log() {
dateString="$(date +%Y-%m-%d' '%R:%S,%N%z | sed 's/\(.*\)......\(.....\)/\1\2/')"
logLevel=$(printf '%-5s' "${1:-INFO}")
className="$0"
processId="$$"
if [ -z "$2" ] ; then
while read -r line ; do
logLine=$(jq -nc --arg dt "$dateString" --arg tp "$logLevel" --arg cn "$className" --arg pr "$processId" --arg msg "$line" '{datetime:$dt,type:$tp,classname:$cn,process:$pr,message:$msg}')
echo "${logLine}"
done
else
logLine=$(jq -nc --arg dt "$dateString" --arg tp "$logLevel" --arg cn "$className" --arg pr "$processId" --arg msg "$2" '{datetime:$dt,type:$tp,classname:$cn,process:$pr,message:$msg}')
echo "${logLine}"
fi
}
if [ -n "${SIGNUM_BACKEND}" ] ; then
SIGNUM_BACKEND="${SIGNUM_BACKEND^^}"
else
SIGNUM_BACKEND="SIGNUM"
fi
echo "backend=${SIGNUM_BACKEND^^}" >> /etc/keyfactor/config
if [ "${SIGNUM_BACKEND}" != "SIGNUM" ] && [ "${SIGNUM_BACKEND}" != "SIGNSERVER" ] ; then
log "ERROR" "SIGNUM_BACKEND must be SIGNUM or SIGNSERVER (got: ${SIGNUM_BACKEND})"
exit 1
fi
log "INFO" "Using backend: ${SIGNUM_BACKEND}"
if [ -n "${SIGNUM_HOSTNAME}" ] ; then
log "INFO" "Setting Signum hostname to: ${SIGNUM_HOSTNAME}"
else
log "ERROR" "Must provide SIGNUM_HOSTNAME"
exit 1
fi
if [ -n "${SIGNUM_CERTIFICATE_PATH}" ] ; then
if [[ "${SIGNUM_CERTIFICATE_PATH}" != /* ]] ; then
log "ERROR" "SIGNUM_CERTIFICATE_PATH must be an absolute path"
exit 1
fi
if [ ! -f "${SIGNUM_CERTIFICATE_PATH}" ] ; then
log "ERROR" "Certificate file not found: ${SIGNUM_CERTIFICATE_PATH}"
exit 1
fi
log "INFO" "Using certificate for authentication: ${SIGNUM_CERTIFICATE_PATH}"
elif [ "${SIGNUM_BACKEND}" = "SIGNSERVER" ] ; then
log "ERROR" "SIGNSERVER backend requires SIGNUM_CERTIFICATE_PATH"
exit 1
elif [ -n "${SIGNUM_USERNAME}" ] ; then
log "INFO" "Setting Signum username to: ${SIGNUM_USERNAME}"
else
log "ERROR" "Must provide either SIGNUM_CERTIFICATE_PATH or SIGNUM_USERNAME"
exit 1
fi
if [ -n "${SIGNUM_PASSWORD}" ] ; then
log "INFO" "Setting Signum password to: xxxxxxxx"
else
log "ERROR" "Must provide SIGNUM_PASSWORD"
exit 1
fi
if [ -n "${SIGNUM_WAF_PORT}" ] ; then
log "INFO" "Setting WAF port to: ${SIGNUM_WAF_PORT}"
fi
if [ -n "${SIGNUM_LOGLEVEL}" ] ; then
# NONE|LOW|HIGH
log "INFO" "Setting Signum log level to: ${SIGNUM_LOGLEVEL}"
echo "loglevel=${SIGNUM_LOGLEVEL^^}" >> /etc/keyfactor/config
else
SIGNUM_LOGLEVEL="LOW"
log "INFO" "Using default log level LOW"
echo "loglevel=${SIGNUM_LOGLEVEL^^}" >> /etc/keyfactor/config
fi
if [ -n "${SIGNUM_LOGTYPE}" ] ; then
# STDOUT|FILE
log "INFO" "Setting Signum log type to: ${SIGNUM_LOGTYPE^^}"
sed -i "s|logtype=.*|logtype=${SIGNUM_LOGTYPE^^}|" /etc/keyfactor/config
else
log "INFO" "Using default log type STDOUT"
fi
if [ -n "${SIGNUM_HTTPS_PROXY}" ] ; then
log "INFO" "Setting Signum HTTPS PROXY to: ${SIGNUM_HTTPS_PROXY^^}"
else
log "INFO" "Using default proxy configuration."
fi
if [ -n "${SIGNUM_AGENTID}" ] ; then
log "INFO" "Setting Signum AgentID to: ${SIGNUM_AGENTID^^}"
echo "{$SIGNUM_HOSTNAME}={$SIGNUM_AGENTID}" > /etc/keyfactor/agentIds
else
log "INFO" "Setting Signum AgentID to: AAAAA-BBBBB-CCCCC-DDDDD"
echo "{$SIGNUM_HOSTNAME}={AAAAA-BBBBB-CCCCC-DDDDD}" > /etc/keyfactor/agentIds
fi
echo "
*****************************************************************************************
* This container is intended to be used with Signums PKCS11 driver for native *
* Linux tool signing. Please visit the URL below for examples of signing with the *
* Signum PKCS11 driver. *
* *
* URL: https://docs.keyfactor.com *
* *
*****************************************************************************************
"
log "INFO" "Starting Keyfactor Signum Agent"
SignumService &
sleep 1
log "INFO" "Calling signum-util command"
if [ "${SIGNUM_BACKEND}" = "SIGNSERVER" ] ; then
SIGNUM_UTIL_ARGS=(-h "${SIGNUM_HOSTNAME}" -c "${SIGNUM_CERTIFICATE_PATH}" -l "${SIGNUM_LOGLEVEL}")
[ -n "${SIGNUM_PASSWORD}" ] && SIGNUM_UTIL_ARGS+=(-p "${SIGNUM_PASSWORD}")
signum-util dss "${SIGNUM_UTIL_ARGS[@]}" | log "INFO"
elif [ -n "${SIGNUM_CERTIFICATE_PATH}" ] ; then
SIGNUM_UTIL_ARGS=(-h "${SIGNUM_HOSTNAME}" -c "${SIGNUM_CERTIFICATE_PATH}" -l "${SIGNUM_LOGLEVEL}")
[ -n "${SIGNUM_PASSWORD}" ] && SIGNUM_UTIL_ARGS+=(-p "${SIGNUM_PASSWORD}")
[ -n "${SIGNUM_WAF_PORT}" ] && SIGNUM_UTIL_ARGS+=(-w "${SIGNUM_WAF_PORT}")
[ -n "${SIGNUM_HTTPS_PROXY}" ] && SIGNUM_UTIL_ARGS+=(-x "${SIGNUM_HTTPS_PROXY}")
signum-util setup "${SIGNUM_UTIL_ARGS[@]}" | log "INFO"
else
SIGNUM_UTIL_ARGS=(-h "${SIGNUM_HOSTNAME}" -u "${SIGNUM_USERNAME}" -p "${SIGNUM_PASSWORD}" -l "${SIGNUM_LOGLEVEL}")
[ -n "${SIGNUM_HTTPS_PROXY}" ] && SIGNUM_UTIL_ARGS+=(-x "${SIGNUM_HTTPS_PROXY}")
signum-util setup "${SIGNUM_UTIL_ARGS[@]}" | log "INFO"
fi
echo "Waiting for Keyfactor Signum service to finish"
wait < <(jobs -p)
echo "All processes finished. Container will shut down"
The script writes to
/etc/keyfactorat runtime, so that directory must be
writable by the container user — which the Dockerfiles below ensure via the
gid 0+chmod -R g=uownership setup.
Dockerfile — Ubuntu 22.04
# syntax=docker/dockerfile:1
FROM ubuntu:22.04
ARG TARGETARCH
# Package file placed next to this Dockerfile, e.g. signum-agent-amd64.deb
ARG AGENT_DEB=signum-agent-${TARGETARCH}.deb
# User/group the agent runs as. gid 0 (root group) + `chmod g=u` is the
# OpenShift convention: unprivileged user, group-writable files.
ARG APP_UID=10001
ARG APP_GID=0
COPY ${AGENT_DEB} /tmp/signum-agent.deb
COPY start.sh /usr/local/keyfactor/bin/start.sh
# --- Install runtime deps, unpack the agent, then drop build-only packages ---
# The agent package is *extracted* (dpkg-deb -x) rather than installed: its
# post-install script would try to enable/start the systemd service, which
# fails during an image build. Extraction lays down the same files without
# running maintainer scripts.
# software-properties-common and gnupg are only needed to add the .NET PPA, so
# they (and the deps they pull in) are purged in the same layer to keep them
# out of the final image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
software-properties-common ca-certificates gnupg \
&& add-apt-repository -y ppa:dotnet/backports \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
aspnetcore-runtime-10.0 \
dotnet-runtime-10.0 \
libpcsclite1 \
libsecret-1-0 \
jq \
unzip \
&& dpkg-deb -x /tmp/signum-agent.deb / \
&& apt-get purge -y --auto-remove software-properties-common gnupg \
&& apt-get clean \
&& rm -rf /tmp/signum-agent.deb /var/lib/apt/lists/*
# --- Config, PKCS#11 provider registration, and non-root user ---
RUN chmod a+x /usr/local/keyfactor/bin/start.sh \
&& sed -i 's/\r$//' /usr/local/keyfactor/bin/start.sh \
&& printf "port=51599\nlogtype=STDOUT\n" > /etc/keyfactor/config \
&& printf 'name = SignumPKCS11\nlibrary = /usr/lib/libsignumpkcs11.so\ndescription = Keyfactor PKCS#11 interface for SmartCard\n' > /etc/keyfactor/signumpkcs11.cfg \
&& mkdir -p /usr/share/p11-kit/modules \
&& printf 'module:/usr/lib/libsignumpkcs11.so\n' > /usr/share/p11-kit/modules/signum.module \
&& { useradd -u ${APP_UID} -g ${APP_GID} -M -d /etc/keyfactor/user keyfactor 2>/dev/null || true; } \
&& mkdir -p /etc/keyfactor/user \
&& chown -R ${APP_UID}:${APP_GID} /usr/local/keyfactor /etc/keyfactor \
&& chmod -R g=u /usr/local/keyfactor /etc/keyfactor
ENV HOME="/etc/keyfactor/user" \
PATH="$PATH:/usr/local/keyfactor/setup:/usr/local/keyfactor/service"
WORKDIR /etc/keyfactor
USER ${APP_UID}:${APP_GID}
CMD ["/usr/local/keyfactor/bin/start.sh"]
Dockerfile — AlmaLinux 9 (CentOS replacement)
# syntax=docker/dockerfile:1
FROM almalinux:9-minimal
ARG TARGETARCH
# Package file placed next to this Dockerfile, e.g. signum-agent-x86_64.rpm
ARG AGENT_RPM=signum-agent-${TARGETARCH}.rpm
# User/group the agent runs as. gid 0 (root group) + `chmod g=u` is the
# OpenShift convention: unprivileged user, group-writable files.
ARG APP_UID=10001
ARG APP_GID=0
# --- Install the .NET runtime and PKCS#11 dependencies ---
# install_weak_deps=0 skips optional recommended packages to keep the image lean.
RUN microdnf install --assumeyes --nodocs --setopt=install_weak_deps=0 \
aspnetcore-runtime-10.0 \
dotnet-runtime-10.0 \
openssl-pkcs11 \
pcsc-lite-libs \
libsecret \
jq \
unzip \
&& microdnf clean all \
&& rm -rf /var/cache/dnf /var/cache/yum
# --- Unpack the agent package ---
# --noscripts skips the post-install scriptlet (which would try to start the
# systemd service and fail during a build); --nodeps because the runtime
# dependencies were already installed above.
COPY ${AGENT_RPM} /tmp/signum-agent.rpm
RUN rpm -i --noscripts --nodeps /tmp/signum-agent.rpm \
&& rm -f /tmp/signum-agent.rpm
# --- Entrypoint, default config and PKCS#11 provider registration ---
COPY start.sh /usr/local/keyfactor/bin/start.sh
RUN chmod a+x /usr/local/keyfactor/bin/start.sh \
&& sed -i 's/\r$//' /usr/local/keyfactor/bin/start.sh \
&& printf "port=51599\nlogtype=STDOUT\n" > /etc/keyfactor/config \
&& printf 'name = SignumPKCS11\nlibrary = /usr/lib/libsignumpkcs11.so\ndescription = Keyfactor PKCS#11 interface for SmartCard\n' > /etc/keyfactor/signumpkcs11.cfg \
&& mkdir -p /usr/share/p11-kit/modules \
&& printf 'module:/usr/lib/libsignumpkcs11.so\n' > /usr/share/p11-kit/modules/signum.module
# --- Run as a non-root user (OpenShift-friendly: uid 10001, gid 0) ---
RUN mkdir -p /etc/keyfactor/user \
&& chown -R ${APP_UID}:${APP_GID} /usr/local/keyfactor /etc/keyfactor \
&& chmod -R g=u /usr/local/keyfactor /etc/keyfactor
ENV HOME="/etc/keyfactor/user" \
PATH="$PATH:/usr/local/keyfactor/setup:/usr/local/keyfactor/service"
WORKDIR /etc/keyfactor
USER ${APP_UID}:${APP_GID}
CMD ["/usr/local/keyfactor/bin/start.sh"]
Build Your Own Signum Agent Container Image
Ubuntu
docker build -f Dockerfile.ubuntu22 -t signum-agent:ubuntu22 .
AlmaLinux 9
docker build -f Dockerfile.alma9 -t signum-agent:alma9 .
Overriding the Runtime User
The Agent runs as an unprivileged user (UID 10001, GID 0). It never runs as
root. Override the defaults at build time if your environment requires a
specific ID:
docker build -f Dockerfile.alma9 \
--build-arg APP_UID=12345 \
--build-arg APP_GID=0 \
-t signum-agent:alma9 .
Keep APP_GID=0 unless you have a reason to change it: group 0 combined with chmod -R g=u is what lets the container tolerate the platform overriding the UID at runtime (as OpenShift does) while keeping its config directory writable. If you set a non-zero GID, the container must be started with that same GID or the Agent will fail to write to /etc/keyfactor.
Using Another Architecture
To build for a different CPU architecture, for example ARM on an x86 host, use
docker buildx:
docker buildx build --platform linux/arm64 -f Dockerfile.alma9 \
-t signum-agent:alma9-arm64 --load .
Running
Keyfactor publishes a companion signum-container-agent repository with
examples that build on a Signum Agent base image:
-
Layering signing tools (Cosign, Jarsigner, Jsign, OpenSC, OpenSSL, XMLSecTool).
-
A full Kubernetes deployment manifest, including running as UID
10001and
injecting a custom CA via an init container. -
Troubleshooting for connectivity, PKCS#11, and permission issues.
Testing
With the Dockerfiles, the .rpm/deb and the start.sh on the same directory:
docker build -f Dockerfile.alma9 \
--build-arg AGENT_RPM=arm64_alma9_keyfactor-agent-4.80.2-e0747fb.rpm \
-t signum-agent:alma9 .
docker build -f Dockerfile.ubuntu22 \
--build-arg AGENT_DEB=arm64_ubuntu22.04_keyfactor-agent-4.80.2-e0747fb.deb \
-t signum-agent:ubuntu22 .
Run the container with the following parameters:
docker exec -it "$(docker run -d \
-v $PWD/loginCertificate.p12:/tmp/loginCertificate.p12 \
-e SIGNUM_HOSTNAME=172.24.0.52:6443 \
-e SIGNUM_PASSWORD=foo123 \
-e SIGNUM_LOGLEVEL=HIGH \
-e SIGNUM_LOGTYPE=FILE \
-e SIGNUM_CERTIFICATE_PATH=/tmp/loginCertificate.p12 \
-e SIGNUM_BACKEND=SIGNSERVER \
signum-agent:alma9)" /bin/bash
Once inside the container, run:
signum-util list-certificates
Notes on Efficient Maintenance
The following design notes explain why the build is structured this way and how to keep it
current with minimal effort:
-
The build compiles nothing. It unpacks a pre-built package into a base OS,
which keeps the Dockerfiles short and stable across releases. -
Two mirror Dockerfiles instead of one. The Ubuntu and AlmaLinux files
differ only in base image and package-manager commands. Keeping them separate
(rather than one file with conditionals) makes each easy to read and maintain. -
Pin only what you must. The base OS tags (
ubuntu:22.04,
almalinux:9-minimal) receive security updates within their major version, so
you get patches by rebuilding without changing the Dockerfile. -
Prefer the standalone package to eliminate the .NET runtime dependency —
then there is nothing version-pinned left to track. -
Rebuild on a schedule (e.g. monthly, or when a new agent package is
released) to pick up OS security updates. No file edits are needed for routine
refreshes. -
New agent version? Just drop in the new
.deb/.rpmand rebuild — the
Dockerfile does not reference the version number. -
Refreshing
start.sh. The embedded copy is taken from a specific release
(noted next to it). To update, copydocker/imports/start.shfrom the target
release over the embedded block.