How-To: Integrate the Keyserver with GitLab CI¶
This guide shows how to integrate GitLab CI pipelines with the Cryptera Keyserver to sign firmware, binaries, or release artifacts automatically.
You will configure GitLab to obtain an OAuth2 token, request a signing operation, wait for approval, and retrieve a signature and validate it — all from within a .gitlab-ci.yml pipeline.
This guide assumes:
- That you have an GitLab clour or on-premise installation and
- GitLab CI runners are already configured
- Keyserver has been configured to your setup for GitLab JWT or OAuth2 Client Credentials
- You know the Keyserver URL and your assigned URL: *.
<UNIQUE-ID>.cryptera-security.io - A Keyserver client subject exists for your service connection
- A signing key is already configured and has the appropriate approval requirements
Production-Ready Authentication
This guide uses GitLab’s OIDC/JWT federation, which is appropriate and recommended for
production environments.
GitLab-issued identity tokens should always be used instead of the internal Authserver.
The internal Authserver is intended only for demo and testing environments.
Do Not Use Internal Authserver in Production
Production CI pipelines must authenticate using GitLab OIDC ($CI_JOB_JWT). The internal Authserver must never be used for production firmware signing or certificate
issuance pipelines.
Integration Overview¶
GitLab CI integrates with the Keyserver in two possible ways:
Option A — For production setup: Using GitLab’s JWT (Recommended)¶
GitLab issues an ephemeral JWT to each pipeline job.
The Keyserver is configured to trust GitLab as an identity provider.
Benefits: - No stored secrets
- Least-privilege per pipeline
- One-time credentials per job
Option B — For initial testing: Using Client Credentials¶
A static client ID + secret is stored as GitLab CI variables.
Benefits: - Simpler setup
This guide documents both approaches.
1. Configure Authentication¶
Option A: GitLab JWT (Recommended)¶
The default GitLab configuration is setup to use "project_id" as JWT claim. This allows for individual permissions for each project.
Configure Subjects¶
In Keyserver Admin UI:
- Create a client subject with the "Subject ID" matching the GitLab project_id
- Project ID can be seen here:
- Go to project page
- Click Settings -> General
- Scroll to "Project ID" and note the id
- Configure its roles (e.g.,
orderer:key1)
If using GitLab On-Prem¶
If a Gitlab On-Prem is being used you must supply the following to Cryptera DS, during provisioning of your system: - Issuer URL: eg. https://gitlab.customer-a.com - If your setup is not accessible from our backend you must also provide a JWK: - Access from https://gitlab.customer-a.com/-/jwks - Note that if you change the keypair for your gitlab instance, you must provide a new JWK
Configure Pipeline¶
For any pipeline that needs to access keyserver, set audience to "keyserver" for the ID Token:
Option B: Client Credentials¶
For the initial test setup a Crytpera authentication server can be used.
Create GitLab CI variables:
KS_CLIENT_IDKS_CLIENT_SECRET
Then request a token in the pipeline using:
export AUTH_CREDS=$(echo -n "$KS_CLIENT_ID:$KS_CLIENT_SECRET" | base64)
export TOKEN=$(curl -sS --tlsv1.3 \
-X POST \
-H "Authorization: Basic $AUTH_CREDS" \
-d "grant_type=client_credentials" \
"https://authserver.<UNIQUE-ID>.cryptera-security.io/oauth2/token" \
| jq -r .access_token)
2. Example GitLab CI Pipeline¶
Below is a minimal working .gitlab-ci.yml pipeline that:
- Builds LibPKCS#11 Keyserver library from sources off Github
- Authenticates (JWT or Client Credentials, this example uses JWT)
- Hashes a test file
- Creates an operation
- Waits for approval
- Requests the signature
- Verifies the signature
Adapt variables as needed.
Example: .gitlab-ci.yml¶
stages:
- build
- await_approval
- sign
variables:
# This is the URL of your keyserver.
KSC_API_SERVER: "https://keyserver.<UNIQUE-ID>.cryptera-security.io"
# This is the hash algorithm that will be used for signing in the sign_binary job.
HASH_ALGORITHM: "-sha256"
# This is the key ID that will be used for signing in the sign_binary job.
# It should match the key ID that you have set up in your key server for production use.
# In a real setup, you would replace "key1" with the actual key ID you want to use for signing.
KEYID: "key1"
# Description that will be attached to the signing operation for auditing purposes.
# It should help approvers understand the context of the operation when asked to approve
KSC_OPERATION_DESCRIPTION: "GitLab PKCS11 signing"
# Sources to build from
LIBPKCS11KS_REPO: "https://github.com/cryptera-device-security/libpkcs11ks.git"
PKCS11MOD_REPO: "https://github.com/cryptera-device-security/pkcs11mod.git"
# Approval wait period in seconds (e.g., 120 seconds)
WAIT_FOR_APPROVAL: 120
# (Optional, Default: 10s) LibPKCS11ks client time out for calls against Keyserver
KSC_HTTP_CLIENT_TIMEOUT: "15s"
build_libpkcs11ks:
stage: build
image: golang:1.20
variables:
CGO_ENABLED: 1
GOOS: linux
GOARCH: amd64
before_script:
- apt-get update && apt-get install -y git
- cd $CI_PROJECT_DIR
# Clone both repos
- git clone "$PKCS11MOD_REPO"
- git clone "$LIBPKCS11KS_REPO"
# Set up pkcs11mod
- cd pkcs11mod
- go mod init github.com/namecoin/pkcs11mod
- go mod tidy
- go generate ./...
script:
# Build libpkcs11ks with pkcs11mod as a replace target
- cd $CI_PROJECT_DIR/libpkcs11ks
- go mod edit -replace github.com/namecoin/pkcs11mod=../pkcs11mod
- go mod tidy
- chmod +x build.sh
- ./build.sh
artifacts:
paths:
- libpkcs11ks/libpkcs11ks.so
expire_in: 1 week
request_operation_await_approval:
stage: await_approval
image: ubuntu:24.04
# When using GitLab's built-in JWT, set the audience to "keyserver", and make sure your keyserver instance is configured to accept GitLab's JWTs.
id_tokens:
KSC_ID_TOKEN:
aud: "keyserver"
before_script:
- export DEBIAN_FRONTEND=noninteractive
- apt-get update
- apt-get install -y jq curl
script: |
set -Eeuo pipefail
echo "== Create operation for $KEYID =="
KSC_OPERATION_ID=$(curl -sS --tlsv1.3 -X 'POST' \
"$KSC_API_SERVER/api/operations" \
-H "Authorization: Bearer $KSC_ID_TOKEN" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"validms": 600000,
"description": "'"$KSC_OPERATION_DESCRIPTION"'",
"new-keyusagerestrictions": [
{
"keyid": "'"$KEYID"'",
"maxusagecount": 1
}
]
}' | jq -r .id)
if [ -z "$KSC_OPERATION_ID" ] || [ "$KSC_OPERATION_ID" = "null" ]; then
echo "Failed to obtain KSC_OPERATION_ID"
exit 1
fi
echo "Operation ID $KSC_OPERATION_ID"
# At this point, you would typically notify the relevant approvers that an operation is awaiting their approval.
# This could be done via email, Slack, or any other communication tool your team uses.
# The notification should include details about the operation
# Note that when using keys that does not require approval, approved will be true immediately and the loop will exit without waiting.
echo "== Wait for approval (max ${WAIT_FOR_APPROVAL}s) =="
for ((i = 1; i <= WAIT_FOR_APPROVAL / 5; i++)); do
STATUS=$(
curl -sS --tlsv1.3 -X GET "$KSC_API_SERVER/api/operations/$KSC_OPERATION_ID" \
-H "Authorization: Bearer $KSC_ID_TOKEN" \
-H 'accept: application/json' | jq -r .approved
)
if [ "$STATUS" = "true" ]; then
echo "Operation ID $KSC_OPERATION_ID is approved"
break
else
echo "Not approved yet"
sleep 5
fi
done
echo "Status $STATUS"
[ "$STATUS" = "true" ] || { echo "Approval timed out"; exit 1; }
echo "KSC_OPERATION_ID=$KSC_OPERATION_ID" > approval.env
artifacts:
reports:
dotenv: approval.env
expire_in: 1 hour
sign_binary:
stage: sign
image: ubuntu:24.04
needs:
- job: build_libpkcs11ks
- job: request_operation_await_approval
id_tokens:
KSC_ID_TOKEN:
aud: "keyserver"
before_script: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl jq openssl libengine-pkcs11-openssl
script: |
set -Eeuo pipefail
mkdir -p "$CI_PROJECT_DIR/scripts"
cp -f "$CI_PROJECT_DIR/libpkcs11ks/libpkcs11ks.so" "$CI_PROJECT_DIR/scripts/libpkcs11ks.so"
echo "== Generate OpenSSL PKCS#11 config =="
cat >"$CI_PROJECT_DIR/scripts/openssl-ks.cnf" <<EOF
openssl_conf = openssl_init
[openssl_init]
engines = engine_section
providers = provider_sect
[provider_sect]
default = default_sect
base = base_sect
[default_sect]
activate = 1
[base_sect]
activate = 1
[engine_section]
pkcs11 = pkcs11_section
[pkcs11_section]
engine_id = pkcs11
dynamic_path = /usr/lib/x86_64-linux-gnu/engines-3/libpkcs11.so
MODULE_PATH = $CI_PROJECT_DIR/scripts/libpkcs11ks.so
init = 0
EOF
export OPENSSL_CONF="$CI_PROJECT_DIR/scripts/openssl-ks.cnf"
#export KSC_API_SERVER="$KSC_API_SERVER"
#export KSC_OPERATION_DESCRIPTION="$KSC_OPERATION_DESCRIPTION"
#: "${KSC_OPERATION_ID:?Missing KSC_OPERATION_ID from request_production_approval job}"
echo "Using approved Operation ID: $KSC_OPERATION_ID"
echo "== Sign test file with OpenSSL + PKCS#11 =="
echo "Test" > test.txt
openssl dgst \
-engine pkcs11 \
-keyform engine \
-sign "pkcs11:object=${KEYID}" \
${HASH_ALGORITHM} \
-out test.txt.sig \
test.txt
ls -l test.txt.sig
echo "== Get public key for the signing key, as part of validation step =="
curl -sS --tlsv1.3 \
-H "Authorization: Bearer $KSC_ID_TOKEN" \
-H "Accept: application/json" \
"$KSC_API_SERVER/api/keys/$KEYID" \
| jq -er '.certificate' > certificate.pem
# Extract pubkey from cert
openssl x509 -in certificate.pem -pubkey -noout > pubkey.pem
echo "== Verify signature against public key =="
openssl dgst \
-verify pubkey.pem \
${HASH_ALGORITHM} \
-signature test.txt.sig \
test.txt
echo "Signature verification OK"
artifacts:
name: "signed-output-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
when: always
expire_in: 14 days
paths:
- scripts/openssl-ks.cnf
- scripts/libpkcs11ks.so
- test.txt
- test.txt.sig
- certificate.pem
- pubkey.pem
3. Approvals in GitLab¶
If the signing key requires approvals:
- GitLab will wait for human approvers in the Approval page
- The job polls
/api/operations/<keyid>until approval is received - If approval does not occur within the retry window, the job fails
You may want to:
- Notify approvers via Slack/email
- Use GitLab’s manual job stage
- Require merge approvals before running signing stages
Summary¶
In this guide you:
- Built Keyserver pkcs11 library
- Configured GitLab CI to authenticate to the Keyserver
- Created a signing operation from a CI pipeline
- Waited for human approval
- Performed a signing request via OpenSSL
- Verified the signature
- Integrated end-to-end signing into your CI/CD workflow
This pattern allows secure, automated firmware or software signing across all GitLab-based development teams.