docs/documentation/platform/pki/code-signing/pkcs11-module.mdx
The Infisical PKCS#11 module is a small native library (.so, .dylib, or .dll) that exposes your Infisical Signers to any tool that supports the PKCS#11 v2.40 standard. Tools like jarsigner, osslsigncode, cosign, apksigner, openssl, and gpg work without modification. They make their usual PKCS#11 calls; the module forwards them to Infisical and returns the signature.
You authenticate as a member of the Signer, using either a Machine Identity or your own Infisical access token. To use a Machine Identity, set it up once:
Grab the pre-built binary for your platform from the releases page:
| Platform | File |
|---|---|
| Linux x86_64 | libinfisical-pkcs11.so |
| Linux ARM64 | libinfisical-pkcs11.so |
| macOS x86_64 | libinfisical-pkcs11.dylib |
| macOS ARM64 | libinfisical-pkcs11.dylib |
| Windows x86_64 | libinfisical-pkcs11.dll |
Drop the binary in a known location:
# Linux
sudo cp libinfisical-pkcs11.so /usr/local/lib/
sudo chmod 755 /usr/local/lib/libinfisical-pkcs11.so
# macOS
sudo cp libinfisical-pkcs11.dylib /usr/local/lib/
sudo chmod 755 /usr/local/lib/libinfisical-pkcs11.dylib
If you want to build instead of downloading, you need Go 1.24+ and a C compiler:
git clone https://github.com/Infisical/infisical-pkcs-11.git
cd infisical-pkcs-11
make build
The output binary lands in the current directory.
The module reads its config from /etc/infisical/pkcs11.conf (override the path with INFISICAL_CONFIG):
{
"server_url": "https://app.infisical.com",
"log_level": "info"
}
Then export the authentication credentials as environment variables:
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="<machine-identity-client-id>"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="<machine-identity-client-secret>"
Instead of a machine identity, you can hand the module an Infisical access token directly, either your own or a machine identity's. Set INFISICAL_TOKEN and the module uses it as-is:
export INFISICAL_SERVER_URL="https://app.infisical.com"
export INFISICAL_TOKEN="<your-access-token>"
If you put the config somewhere other than /etc/infisical/pkcs11.conf:
export INFISICAL_CONFIG=/path/to/your/pkcs11.conf
| Field | Required | Default | Description |
|---|---|---|---|
server_url | Yes | None | Infisical server URL. Must use http:// (local dev) or https://. |
auth.method | No | inferred | Authentication method: universal-auth or token. Inferred from the credentials when unset (a token means token, otherwise universal-auth). |
auth.client_id | No | None | Machine identity client ID, for universal-auth. Prefer the env var. |
auth.client_secret | No | None | Machine identity client secret, for universal-auth. Prefer the env var. |
auth.token | No | None | An Infisical access token, for token auth. Prefer the env var. |
tls.ca_cert_path | No | None | Custom CA bundle path for self-hosted Infisical with a private CA. |
tls.skip_verify | No | false | Skip TLS verification (development only, never enable in production). |
cache.token_ttl_seconds | No | 300 | How long the auth token is cached. |
cache.cert_ttl_seconds | No | 3600 | How long fetched certificates are cached. |
cache.signer_ttl_seconds | No | 300 | How long the list of Signers is cached. |
approval.signing_duration | No | None | Auto-request signing access with this window when no active access exists. Valid range: 1m to 30d. |
approval.signing_count | No | None | Auto-request signing access for this many signatures when no active access exists. |
approval.exclude_scope_fields | No | None | Signing parameters to leave out of the requests the module opens, so one approval covers any value of them. Any of command, signing_application, signing_application_hash, hostname, os_username, ip_address, data_hash. An unknown name is rejected at startup. |
approval.ip_address | No | None | Pin the requests the module opens to this address instead of the one Infisical sees them arrive from, which is what it uses when this is unset. It does not have to be this machine's, so you can name a build agent's egress address. |
log_level | No | info | One of trace, debug, info, warn, error. |
log_file | No | stderr | Optional path to a log file. |
| Variable | Description |
|---|---|
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID | Machine identity client ID. Overrides config. |
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET | Machine identity client secret. Overrides config. |
INFISICAL_TOKEN | An Infisical access token (a user or machine identity token). Selects token auth; used instead of the client ID/secret. |
INFISICAL_SERVER_URL | The Infisical instance URL. Sets server_url (and overrides the config file). |
INFISICAL_CONFIG | Path to the config file (default /etc/infisical/pkcs11.conf). |
Use pkcs11-tool (from OpenSC: brew install opensc or apt install opensc) to confirm everything is wired up.
pkcs11-tool \
--module /usr/local/lib/libinfisical-pkcs11.so \
--list-slots
You should see one slot per Signer your machine identity is a member of:
Available slots:
Slot 0 (0x0): mobile-app-prod
token label : mobile-app-prod
token manufacturer : Infisical
token model : Code Signing
...
Slot 1 (0x1): release-signer
...
To list the objects (private key, public key, X.509 cert) in a slot:
pkcs11-tool \
--module /usr/local/lib/libinfisical-pkcs11.so \
--slot 0 \
--list-objects
A quick smoke-test sign:
echo "hello" > /tmp/payload.bin
pkcs11-tool \
--module /usr/local/lib/libinfisical-pkcs11.so \
--slot 0 \
--sign --mechanism SHA256-RSA-PKCS \
--input-file /tmp/payload.bin \
--output-file /tmp/payload.sig
If the Signer has an approval policy and you don't have active access, the sign call is rejected with CKR_GENERAL_ERROR and the module logs a "signing requires approval" line. See Automatic Signing Access Requests below to make this seamless.
When a Signer has a policy attached, sign calls without active access normally fail. The module can automatically open a signing request for you on the first denied call. You just need to add an approval block to the config:
{
"server_url": "https://app.infisical.com",
"approval": {
"signing_duration": "8h",
"signing_count": 10
}
}
signing_duration requests an access window of this duration (30m, 8h, 2d; range 1m to 30d).signing_count requests access good for this many signing operations.Set one or both depending on what the Signer's policy expects. The values are capped at the policy's maxWindow / maxSignings.
The request is scoped to what the module saw: the command, the tool, the machine, the OS account, and the artifact. Approvers review the real signing situation, and the approval only covers that situation.
<Note> The first sign call **still fails**, because an approver has to act on the request. Once approved, run the same command again and it succeeds.The approval covers that exact command and artifact, so a different artifact, a changed argument, or reordered flags needs a new request. A request's parameters are fixed once it is open, so when they are wrong the request is rejected and reopened rather than edited. To have one approval cover a batch, leave the parameters that vary out of the request from the start. </Note>
Every parameter on a request has to match at signing time, so a request that names the artifact hash covers exactly one artifact. To have one approval cover a series of builds, name the parameters that vary in approval.exclude_scope_fields and the module leaves them off every request it opens:
{
"approval": {
"signing_duration": "8h",
"signing_count": 10,
"exclude_scope_fields": ["data_hash"]
}
}
data_hash is the usual one. A timestamped signature changes the digest between runs even for the same file, so pinning it means a fresh approval for every build. The other names are command, signing_application, signing_application_hash, hostname, os_username and ip_address.
Leave out only what genuinely varies, and be aware that dropping a parameter widens the approval further than the machine you configured it on. An approval belongs to the identity that asked for it, not to a host, so once hostname is excluded the same approval covers that identity signing from anywhere its credentials are installed.
An excluded parameter is still recorded on the signing operation, so the command, machine, account and artifact hash behind every signature stay available to auditors even when they no longer constrain the approval.
The address is the one parameter the module cannot observe, because what counts is the address Infisical receives the sign call from, after any NAT or proxy in between. Infisical fills it in from the request itself, the same value it records in the audit log, so requests are scoped by address without any configuration.
Two settings change that:
{
"approval": {
"ip_address": "203.0.113.10",
"exclude_scope_fields": ["ip_address"]
}
}
ip_address pins a different address than the request came from, which is how you tie an approval to a build agent's egress address from elsewhere. Excluding ip_address leaves signing unrestricted by address, which is what a fleet behind a rotating NAT pool needs, since a pinned address would stop matching between runs. Setting both leaves the address unscoped, because excluding wins.
Infisical always compares against the address it sees, never one a caller reports, so neither setting can widen access beyond what the approval already allows.
jarsigner (or any PKCS#11 tool) against the Infisical module.approval block and auto-creates a signing request with the configured duration and count.For fully unattended CI, have an Administrator pre-approve signing access via Pre-approve signing before the pipeline runs.
If the module fails before it can log anything, it prints the reason to stderr prefixed infisical-pkcs11:. That covers a config file it cannot read, since the log path lives in the file that just failed, and PKCS#11 gives the calling tool nothing but a generic CKR_GENERAL_ERROR.
For everything else, turn on debug logging in the config first. It'll usually tell you exactly what went wrong:
{
"log_level": "debug",
"log_file": "/tmp/infisical-pkcs11.log"
}