Code signing on Linux is fragmented across multiple ecosystems — each package format, container runtime, and distribution channel has its own signing mechanism. Unlike Windows (Authenticode) or macOS (codesign), there’s no single unified signing framework for Linux. This means developers and DevOps engineers need to understand GPG signing for packages, cosign for containers, kernel module signing for drivers, and emerging standards like Sigstore for keyless verification.
This guide covers every major Linux signing workflow, from traditional GPG-based package signing through modern keyless approaches, with practical commands and CI/CD integration examples.
Why Sign Linux Artifacts?
Code signing establishes two critical properties:
- Authenticity: The artifact was produced by the claimed publisher
- Integrity: The artifact hasn’t been modified since signing
Without signing, users and systems cannot distinguish legitimate software from tampered or malicious versions. Supply chain attacks like the SolarWinds compromise and the xz-utils backdoor (CVE-2024-3094) demonstrate why verification of software provenance is essential.
What Should Be Signed?
| Artifact Type | Signing Method | Verification Point |
|---|---|---|
| RPM packages | GPG (rpmsign) | Package manager (dnf/yum) |
| DEB packages | GPG (dpkg-sig, debsign) | Package manager (apt) |
| Container images | cosign, Notary v2 | Container runtime/admission controller |
| Git commits/tags | GPG or SSH | Git hosting platform |
| Kernel modules | X.509 (sign-file) | Kernel module loader |
| AppImages | GPG | AppImage runtime |
| Flatpaks | GPG (OSTree) | Flatpak runtime |
| Generic binaries | GPG detached signatures | Manual verification |
GPG Signing Fundamentals
GPG (GNU Privacy Guard) is the foundation of most Linux signing workflows. Before diving into specific artifact types, let’s establish the GPG basics.
Generating a Signing Key
# Generate a new GPG key for code signing
gpg --full-generate-key
# Recommended settings:
# Key type: RSA (sign only) or ed25519
# Key size: 4096 bits (RSA) or default (ed25519)
# Expiration: 2-3 years (rotate regularly)
# Name: Your Name (Code Signing)
# Email: signing@yourorg.com
# For automated environments, use batch mode:
cat > key-params.txt << 'PARAMS'
%no-protection
Key-Type: eddsa
Key-Curve: ed25519
Key-Usage: sign
Name-Real: Acme Corp
Name-Email: security@acme.com
Expire-Date: 2y
%commit
PARAMS
gpg --batch --gen-key key-params.txt
rm key-params.txt
Key Management Best Practices
# Export public key for distribution
gpg --armor --export security@acme.com > acme-signing-key.asc
# Backup private key securely (offline storage)
gpg --armor --export-secret-keys security@acme.com > private-key-backup.asc
# Store in a hardware token (YubiKey)
gpg --edit-key security@acme.com
# > keytocard
# List signing keys
gpg --list-secret-keys --keyid-format=long
RPM Package Signing
RPM uses GPG signatures embedded directly in the package file.
Setting Up RPM Signing
# Configure RPM macros for signing
cat >> ~/.rpmmacros << 'EOF'
%_signature gpg
%_gpg_name security@acme.com
%__gpg /usr/bin/gpg2
%__gpg_sign_cmd %{__gpg} \
gpg --force-v3-sigs --batch --verbose --no-armor \
--no-secmem-warning -u "%{_gpg_name}" \
-sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename}
EOF
Signing RPM Packages
# Sign a single package
rpmsign --addsign mypackage-1.0.0-1.x86_64.rpm
# Sign multiple packages
rpmsign --addsign *.rpm
# Verify signature
rpm --checksig mypackage-1.0.0-1.x86_64.rpm
# mypackage-1.0.0-1.x86_64.rpm: digests signatures OK
# Import public key on client systems
rpm --import https://packages.acme.com/RPM-GPG-KEY-acme
# Verify with verbose output
rpm -Kv mypackage-1.0.0-1.x86_64.rpm
Repository Signing
# Sign repository metadata (repomd.xml)
gpg --detach-sign --armor repodata/repomd.xml
# Configure yum/dnf to require signatures
cat > /etc/yum.repos.d/acme.repo << 'EOF'
[acme]
name=Acme Packages
baseurl=https://packages.acme.com/el9/
enabled=1
gpgcheck=1
gpgkey=https://packages.acme.com/RPM-GPG-KEY-acme
repo_gpgcheck=1
EOF
DEB Package Signing
Debian packages use a different signing approach with multiple tools available.
Using dpkg-sig
# Sign a .deb package
dpkg-sig -k security@acme.com --sign builder mypackage_1.0.0_amd64.deb
# Verify signature
dpkg-sig --verify mypackage_1.0.0_amd64.deb
# Processing mypackage_1.0.0_amd64.deb...
# GOODSIG _gpgbuilder A1B2C3D4E5F6G7H8
Using debsign (for source packages)
# Sign .dsc and .changes files
debsign -k security@acme.com mypackage_1.0.0_amd64.changes
# This signs both the .dsc (source description) and .changes file
APT Repository Signing
# Generate a repository signing key
gpg --full-generate-key # Use RSA 4096 for repository signing
# Export for the repository
gpg --armor --export security@acme.com > /var/www/repo/KEY.gpg
# Sign Release file
gpg --default-key security@acme.com \
--detach-sign --armor \
--output Release.gpg Release
# Create InRelease (clearsigned, preferred)
gpg --default-key security@acme.com \
--clearsign \
--output InRelease Release
# Client-side: Add repository key
curl -fsSL https://packages.acme.com/KEY.gpg | \
gpg --dearmor -o /usr/share/keyrings/acme-archive-keyring.gpg
# Configure apt source with signed-by
cat > /etc/apt/sources.list.d/acme.list << 'EOF'
deb [signed-by=/usr/share/keyrings/acme-archive-keyring.gpg] https://packages.acme.com/debian stable main
EOF
Kernel Module Signing
Linux kernel module signing ensures only authorized modules can be loaded, critical for Secure Boot and system integrity.
How Kernel Module Signing Works
The kernel uses X.509 certificates (not GPG) for module signing. During kernel build, a signing key is generated and embedded in the kernel. Only modules signed with this key (or keys in the system keyring) can be loaded.
# Check if module signing is enforced
cat /proc/sys/kernel/modules_disabled # 1 = no new modules
cat /proc/keys | grep -i "asymmetric" # Shows trusted keys
# Check a module's signature
modinfo my_module.ko | grep sig
# sig_id: PKCS#7
# signer: Build time autogenerated kernel key
# sig_key: AB:CD:EF:...
# sig_hashalgo: sha256
Signing Custom Kernel Modules
# Generate a signing key pair
openssl req -new -x509 -newkey rsa:4096 \
-keyout signing_key.pem \
-outform DER -out signing_key.x509 \
-nodes -days 365 \
-subj "/CN=Module Signing Key/"
# Sign the module
/usr/src/kernels/$(uname -r)/scripts/sign-file \
sha256 \
signing_key.pem \
signing_key.x509 \
my_module.ko
# Verify the signature
modinfo my_module.ko | grep -A2 "sig"
# Enroll the key for Secure Boot (requires MOK)
mokutil --import signing_key.x509
# Reboot and confirm enrollment in MOK Manager
DKMS with Module Signing
# Configure DKMS to sign modules automatically
cat > /etc/dkms/framework.conf.d/signing.conf << 'EOF'
sign_tool="/usr/lib/linux-kbuild-$(uname -r | cut -d. -f1-2)/scripts/sign-file"
mok_signing_key="/var/lib/dkms/mok.key"
mok_certificate="/var/lib/dkms/mok.pub"
EOF
# Generate MOK key for DKMS
openssl req -new -x509 -newkey rsa:4096 \
-keyout /var/lib/dkms/mok.key \
-outform DER -out /var/lib/dkms/mok.pub \
-nodes -days 730 \
-subj "/CN=DKMS Module Signing/"
mokutil --import /var/lib/dkms/mok.pub
Container Image Signing with Cosign
Cosign (part of the Sigstore project) is the modern standard for container image signing.
Key-Based Signing
# Generate a cosign key pair
cosign generate-key-pair
# Creates cosign.key (private) and cosign.pub (public)
# Sign a container image
cosign sign --key cosign.key registry.acme.com/app:v1.2.3
# Verify the signature
cosign verify --key cosign.pub registry.acme.com/app:v1.2.3
# Sign with annotations (metadata)
cosign sign --key cosign.key \
-a "commit=$(git rev-parse HEAD)" \
-a "pipeline=https://ci.acme.com/builds/123" \
-a "builder=github-actions" \
registry.acme.com/app:v1.2.3
Keyless Signing with Sigstore
Keyless signing uses short-lived certificates from Sigstore’s Fulcio CA, tied to an OIDC identity:
# Keyless signing (uses OIDC identity - GitHub, Google, Microsoft)
cosign sign registry.acme.com/app:v1.2.3
# Opens browser for OIDC authentication
# Certificate issued by Fulcio, logged in Rekor transparency log
# Verify keyless signature
cosign verify \
--certificate-identity=ci@acme.com \
--certificate-oidc-issuer=https://accounts.google.com \
registry.acme.com/app:v1.2.3
Kubernetes Admission Control
# Kyverno policy requiring signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.acme.com/*"
attestors:
- entries:
- keyless:
subject: "ci@acme.com"
issuer: "https://accounts.google.com"
rekor:
url: https://rekor.sigstore.dev
Notary v2 (ORAS Signatures)
Notary v2 provides OCI-native signatures stored alongside images in registries:
# Sign with notation (Notary v2 CLI)
notation sign registry.acme.com/app:v1.2.3 \
--key "signing-key" \
--signature-format cose
# Verify
notation verify registry.acme.com/app:v1.2.3
# List signatures
notation list registry.acme.com/app:v1.2.3
Git Commit and Tag Signing
GPG Signing
# Configure Git to use GPG signing
git config --global user.signingkey A1B2C3D4E5F6G7H8
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Sign a commit
git commit -S -m "feat: add authentication module"
# Sign a tag
git tag -s v1.2.3 -m "Release v1.2.3"
# Verify signatures
git log --show-signature -1
git tag -v v1.2.3
SSH Signing (Git 2.34+)
# Configure Git to use SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Create allowed signers file
echo "user@acme.com $(cat ~/.ssh/id_ed25519.pub)" > ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
# Sign and verify
git commit -S -m "feat: signed with SSH key"
git log --show-signature -1
Gitsign (Sigstore for Git)
# Install gitsign
go install github.com/sigstore/gitsign@latest
# Configure Git to use gitsign
git config --global commit.gpgsign true
git config --global tag.gpgsign true
git config --global gpg.x509.program gitsign
git config --global gpg.format x509
# Commits are now signed with Sigstore (keyless, OIDC-based)
git commit -m "feat: keyless signed commit"
AppImage and Flatpak Signing
AppImage Signing
# Sign an AppImage with GPG
gpg --detach-sign --armor MyApp-x86_64.AppImage
# Embed signature in AppImage (using appimagetool)
appimagetool --sign --sign-key security@acme.com AppDir/ MyApp-x86_64.AppImage
# Verify
gpg --verify MyApp-x86_64.AppImage.asc MyApp-x86_64.AppImage
Flatpak Signing
Flatpak uses OSTree’s GPG signing for repositories:
# Generate a key for Flatpak repository
gpg --homedir=/var/lib/flatpak-repo-gpg --gen-key
# Initialize a signed Flatpak repository
ostree init --repo=repo --mode=archive-z2
flatpak build-sign repo --gpg-sign=KEYID --gpg-homedir=/var/lib/flatpak-repo-gpg
# Export and sign
flatpak build-export --gpg-sign=KEYID repo app-build/
# Users add the repository with the GPG key
flatpak remote-add --gpg-import=acme-flatpak.gpg acme-repo https://flatpak.acme.com/repo
Sigstore for Linux: The Modern Approach
Sigstore provides a complete signing ecosystem that eliminates long-lived key management:
Components
| Component | Purpose |
|---|---|
| Cosign | Container image signing |
| Fulcio | Short-lived certificate authority (OIDC-based) |
| Rekor | Transparency log (immutable record of signatures) |
| Gitsign | Git commit signing |
| Policy Controller | Kubernetes admission control |
How Keyless Signing Works
1. Developer authenticates via OIDC (GitHub, Google, etc.)
2. Fulcio issues a short-lived certificate (10 minutes)
binding the OIDC identity to a signing key
3. Artifact is signed with the ephemeral key
4. Signature + certificate are recorded in Rekor transparency log
5. Ephemeral key is discarded
6. Verification checks Rekor log + certificate chain
Benefits Over Traditional GPG
| Aspect | GPG Signing | Sigstore Keyless |
|---|---|---|
| Key management | Manual, error-prone | No long-lived keys |
| Key rotation | Complex, breaks verification | Automatic (per-signing) |
| Revocation | CRL/keyserver updates | Not needed (short-lived) |
| Identity binding | Email in key UID | OIDC identity (verified) |
| Transparency | None | Public log (Rekor) |
| Offline verification | Yes | Requires Rekor access |
Setting Up a Signing Infrastructure
Centralized Signing Service
For organizations managing multiple projects, a centralized signing service provides consistency and key protection:
# Architecture overview
signing-service:
components:
- name: "Key Storage"
options:
- "HashiCorp Vault (Transit engine)"
- "AWS KMS / GCP Cloud KMS"
- "Hardware Security Module (HSM)"
- name: "Signing API"
description: "Internal service that accepts signing requests"
auth: "mTLS + RBAC"
- name: "Policy Engine"
description: "Validates what can be signed and by whom"
rules:
- "Only CI/CD pipelines can sign release artifacts"
- "Human approval required for production releases"
- "All signing events logged to audit trail"
Vault Transit Engine for Signing
# Enable Vault Transit for signing operations
vault secrets enable transit
# Create a signing key
vault write transit/keys/code-signing \
type=ecdsa-p256 \
exportable=false
# Sign data (base64-encoded)
DIGEST=$(sha256sum artifact.tar.gz | awk '{print $1}' | xxd -r -p | base64)
vault write transit/sign/code-signing \
input="$DIGEST" \
hash_algorithm=sha2-256 \
signature_algorithm=pkcs1v15
# Verify
vault write transit/verify/code-signing \
input="$DIGEST" \
signature="vault:v1:..."
CI/CD Integration
GitHub Actions
name: Build and Sign
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- name: Sign image (keyless)
run: |
cosign sign --yes \
-a "repo=${{ github.repository }}" \
-a "sha=${{ github.sha }}" \
-a "ref=${{ github.ref }}" \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Sign RPM packages
run: |
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import
rpmsign --addsign dist/*.rpm
- name: Generate SBOM and sign
run: |
syft ghcr.io/${{ github.repository }}:${{ github.ref_name }} -o spdx-json > sbom.spdx.json
cosign attest --yes --predicate sbom.spdx.json \
--type spdxjson \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
GitLab CI
sign-artifacts:
stage: sign
image: alpine:latest
variables:
COSIGN_YES: "true"
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
before_script:
- apk add --no-cache cosign gpg rpm-sign
script:
# Sign container image
- cosign sign
--identity-token=$SIGSTORE_ID_TOKEN
${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
# Sign RPM packages
- echo "${GPG_PRIVATE_KEY}" | gpg --import
- rpmsign --addsign dist/*.rpm
# Upload signed artifacts
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN"
--upload-file dist/*.rpm
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/release/${CI_COMMIT_TAG}/"'
rules:
- if: $CI_COMMIT_TAG
Verification Workflows
Automated Verification in Deployment
#!/bin/bash
# verify-artifacts.sh - Run before deployment
set -euo pipefail
REGISTRY="registry.acme.com"
IMAGE="${REGISTRY}/app:${VERSION}"
SIGNER="ci@acme.com"
ISSUER="https://accounts.google.com"
echo "Verifying container image signature..."
cosign verify \
--certificate-identity="${SIGNER}" \
--certificate-oidc-issuer="${ISSUER}" \
"${IMAGE}" || { echo "FAILED: Image signature invalid"; exit 1; }
echo "Verifying SBOM attestation..."
cosign verify-attestation \
--certificate-identity="${SIGNER}" \
--certificate-oidc-issuer="${ISSUER}" \
--type spdxjson \
"${IMAGE}" || { echo "FAILED: SBOM attestation invalid"; exit 1; }
echo "All verifications passed."
RPM Verification on Install
# Ensure gpgcheck is enforced system-wide
sed -i 's/gpgcheck=0/gpgcheck=1/' /etc/yum.conf
# Verify a package before manual install
rpm -K package.rpm
# package.rpm: digests signatures OK
# Check which key signed a package
rpm -qi package.rpm | grep "Signature"
Key Management for Signing Keys
Key Hierarchy
┌─────────────────────────────────────────┐
│ Root Signing Key │
│ (Offline, HSM, used to sign sub-keys) │
└─────────────────┬───────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────────┐
│ Release │ │ CI/CD │ │ Repository │
│ Signing │ │ Signing│ │ Signing │
│ Key │ │ Key │ │ Key │
└────────┘ └────────┘ └────────────┘
Key Rotation Strategy
# Generate new signing key
gpg --full-generate-key
# Cross-sign with old key (establishes trust chain)
gpg --default-key OLD_KEY_ID --sign-key NEW_KEY_ID
# Update repository metadata with new key
rpm --import new-signing-key.asc
# Transition period: sign with both keys
rpmsign --addsign --key-id=NEW_KEY_ID *.rpm
# After transition: revoke old key
gpg --gen-revoke OLD_KEY_ID > revocation.asc
gpg --import revocation.asc
Protecting Signing Keys
| Protection Level | Method | Use Case |
|---|---|---|
| Basic | Passphrase-protected GPG key | Individual developer |
| Standard | Key in CI/CD secrets store | Automated pipelines |
| High | Hardware token (YubiKey) | Release signing |
| Enterprise | HSM (PKCS#11) or Cloud KMS | Organization-wide |
For enterprise environments, platforms like QCecuring provide centralized signing key management with HSM-backed key storage, policy enforcement, and complete audit trails — ensuring signing keys are never exposed to individual systems or developers.
Key Takeaways
- Linux code signing is fragmented — each artifact type (RPM, DEB, container, kernel module) has its own signing mechanism. Plan for all of them.
- Sigstore/cosign is the future for container images — keyless signing eliminates key management complexity while providing stronger identity binding through OIDC.
- GPG remains essential for traditional package signing (RPM, DEB, APT repositories). Invest in proper key management and rotation procedures.
- Kernel module signing is mandatory for Secure Boot environments — use MOK (Machine Owner Key) enrollment for custom modules.
- CI/CD integration is critical — signing should happen automatically in your pipeline, not manually by developers. Use ephemeral keys or centralized signing services.
- Verification must be enforced — signing is only valuable if consumers verify signatures. Configure package managers to require signatures and use Kubernetes admission controllers for containers.
- Protect your signing keys — a compromised signing key undermines all trust. Use hardware tokens, HSMs, or cloud KMS for key storage. Never store signing keys in source control.
- Plan for key rotation — keys expire and algorithms weaken. Design your signing infrastructure to support key rotation without breaking verification of previously signed artifacts.