Custom Container Build

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> is amd64/x86_64 for Intel/AMD or arm64/aarch64 for 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

ubuntu:22.04

.deb

RHEL-compatible

almalinux:9-minimal

.rpm

Note on using AlmaLinux 9 instead of CentOS

CentOS Linux is end-of-life and no longer receives updates, so it is not a
safe base for a signing service. AlmaLinux 9 is a free, community-governed,
1:1 binary-compatible rebuild of RHEL 9 and is the standard drop-in replacement
for CentOS. If your organization has a RHEL subscription, you can swap
almalinux:9-minimal for redhat/ubi9-minimal. The Dockerfile is otherwise
identical.


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

/usr/local/keyfactor/service/

SignumService (the signing service)

/usr/local/keyfactor/setup/

signum-util (the configuration CLI)

/usr/lib/libsignumpkcs11.so

PKCS#11 driver

/etc/keyfactor/

Agent config (config, signum.module, README)

The Dockerfiles below add two more files that the package does not ship but
that signing tools need to locate the token:

Path

Purpose

/etc/keyfactor/signumpkcs11.cfg

PKCS#11 provider config referenced by Java tools (keytool, jarsigner, jsign) via -providerArg / --keystore

/usr/share/p11-kit/modules/signum.module

Registers the driver with p11-kit so p11-kit-aware tools (OpenSSL, pkcs11-tool) discover it automatically

Runtime Dependencies

The final image needs the .NET 10 runtime plus a few libraries:

Purpose

Ubuntu 22.04 package

AlmaLinux 9 package

.NET runtime

aspnetcore-runtime-10.0, dotnet-runtime-10.0

aspnetcore-runtime-10.0, dotnet-runtime-10.0

PKCS#11 / smartcard libs

libpcsclite1

pcsc-lite-libs, openssl-pkcs11

Secure credential storage

libsecret-1-0

libsecret

Startup script helpers

jq, unzip

jq, unzip

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:

  1. Reads the SIGNUM_* environment variables (see Running).

  2. Writes the service configuration to /etc/keyfactor/config and the agent
    identity to /etc/keyfactor/agentIds.

  3. Launches SignumService in the background.

  4. 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:

Bash
#!/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/keyfactor at runtime, so that directory must be
writable by the container user — which the Dockerfiles below ensure via the
gid 0 + chmod -R g=u ownership 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

Bash
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:

Bash
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:

Bash
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 10001 and
    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/.rpm and 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, copy docker/imports/start.sh from the target
    release over the embedded block.