Skip to content

CA-Signed Mode

CA-signed mode uses a Certificate Authority to issue and validate client certificates for Tarsus agents.

ScenarioWhy CA-Signed Mode
Enterprise environmentsCentralized certificate management
Large deploymentsScalable agent onboarding
Compliance requirementsPKI audit trail
Existing PKIReuse existing infrastructure
Automated provisioningCertificate lifecycle management
AspectThumbprintCA-Signed
Certificate issuanceSelf-signed per agentCentral CA
Trust modelIndividual trustHierarchical trust
OnboardingManual per agentAutomated possible
RevocationPer-registrationPer-registration, or CRL
ComplexityLowerHigher
ScalabilityLimitedExcellent
OptionDescriptionComplexity
Self-managed CACreate your own CAMedium
HashiCorp VaultVault PKI secrets engineMedium
AWS Private CAAWS Certificate Manager PCALow
Step CAOpen source ACME CAMedium
Enterprise CAActive Directory CSHigh

Use the shipped tooling first. mantisctl cert generates every certificate Mantis needs with the correct EKUs and SANs — hand-rolled openssl is the source of most cert misconfigurations (wrong EKUs, missing SANs, wrong thumbprints) and should be a last resort for air-gapped hosts without mantisctl.

Terminal window
# Recommended: generate the CA (CN defaults to "Mantis Development CA",
# --days 3650, --output-dir ./certs)
mantisctl cert generate-ca --output-dir /etc/mantis/certs
# Server identity (Thorax listener + Mandible internal gRPC) — --sans is REQUIRED:
mantisctl cert generate-server --name server \
--sans thorax --sans mandible --sans <hosts...> --output-dir /etc/mantis/certs
# Client and the CN-"mandible" dispatch identity:
mantisctl cert generate-client --name client --output-dir /etc/mantis/certs
mantisctl cert generate-client --name dispatch --cn mandible --output-dir /etc/mantis/certs

The generated CA carries basicConstraints: CA:TRUE, pathlen:1 — it may sign one level of intermediate, and that intermediate signs only leaf certificates. A CA you bring yourself needs at least the same depth; an intermediate cannot sign a further CA.

mantisctl creates every private key readable only by its owner, so keys it generates need no follow-up chmod. The permissions below apply to certificates you install by hand.

Every certificate the CA signs expires no later than the CA itself. A request for a longer validity is shortened to the CA’s own expiry rather than issuing a certificate that stops verifying while still presenting itself as valid.

Other helpers: mantisctl cert thumbprint <path>, cert scan-expiry, cert list-expiring, cert acme; and on agents, tarsus rotate-certificate, tarsus cert-import / cert-export.

Mandible signs every certificate this mode issues, so it needs the root’s private key. Thorax forwards an agent’s signing request to Mandible rather than signing it, which is what keeps CA key material off the Thorax fleet: a compromised Thorax yields no ability to mint identities.

Until a root is stored, enrollment fails with No certificate authority is configured. Set [certificate_authority] in mandible.toml and it imports the CA on startup, or import it explicitly:

Terminal window
mantisctl cert import-ca \
--cert /etc/mantis/certs/ca-cert.pem \
--key /etc/mantis/certs/ca-key.pem

Either way the private key is encrypted at rest, bound to the CA record. The import is refused if the key does not match the certificate, or if a platform root is already stored. Certificates the CA issued before the import keep verifying — the CA itself is unchanged.

A tenant may hold its own signing CA, issued as an intermediate beneath the platform root. This is optional: a deployment with none has the root sign every agent certificate directly, which is the default.

Terminal window
# Issue a tenant's CA (platform administrators only)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"validity_days": 825}' \
"$MANTIS_URL/api/v1/tenants/<tenant-id>/certificate-authority"
# Describe it
curl -H "Authorization: Bearer $TOKEN" \
"$MANTIS_URL/api/v1/tenants/<tenant-id>/certificate-authority"

The intermediate carries pathlen:0, so it signs agent certificates and never another CA, and its expiry is clamped to the root’s. Thorax’s trust store is unaffected: every intermediate chains to the root it already trusts.

Infrastructure certificates — the mandible, thorax, client and server common names — must be signed by the platform root directly. A certificate bearing one of those names signed by a tenant intermediate is refused, so a tenant CA cannot mint a control-plane identity.

Only if mantisctl is unavailable. You must set the SANs and EKUs yourself — rustls/webpki matches on SAN (no CN fallback) and enforces the EKU.

Terminal window
# Create CA directory structure
mkdir -p /etc/mantis/ca/{certs,crl,newcerts,private}
chmod 700 /etc/mantis/ca/private
touch /etc/mantis/ca/index.txt
echo 1000 > /etc/mantis/ca/serial
# Generate CA private key
openssl genrsa -aes256 \
-out /etc/mantis/ca/private/ca.key 4096
chmod 400 /etc/mantis/ca/private/ca.key
# Generate CA certificate
openssl req -new -x509 \
-key /etc/mantis/ca/private/ca.key \
-out /etc/mantis/ca/certs/ca.crt \
-days 3650 \
-subj "/CN=Mantis Internal CA/O=Your Organization/C=US"

Create /etc/mantis/ca/openssl.cnf:

[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/mantis/ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
private_key = $dir/private/ca.key
certificate = $dir/certs/ca.crt
default_days = 365
default_md = sha256
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
[ agent_cert ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = DNS:web-prod-01.example.com

serverAuth and the SAN are not optional for listen-mode agents: Thorax dials them, so the agent presents this certificate as a server and rustls verifies the hostname against the SAN. A clientAuth-only certificate fails with UnsupportedCertificate the first time Thorax connects. Poll-mode-only agents can drop serverAuth and the SAN, but issuing both keeps a single template usable for either mode.

[tls]
auth_mode = "ca_signed"
# Server certificate
cert_path = "/etc/mantis/certs/server-cert.pem"
key_path = "/etc/mantis/certs/server-key.pem"
# CA certificate for validating clients
ca_cert_path = "/etc/mantis/ca/certs/ca.crt"
Terminal window
export MANTIS_TLS_AUTH_MODE=ca_signed
export MANTIS_TLS_CERT_PATH=/etc/mantis/certs/server-cert.pem
export MANTIS_TLS_KEY_PATH=/etc/mantis/certs/server-key.pem
export MANTIS_TLS_CA_CERT_PATH=/etc/mantis/ca/certs/ca.crt

Agents presenting a certificate the CA did not sign fall back to thumbprint validation and do follow the approval path. To approve those automatically during onboarding, issue each one a single-use registration token (configured via TARSUS__REGISTRATION__TOKEN). Otherwise they land in the pending queue for manual approval.

mantisctl issues agent certificates with the extensions Mantis requires. Prefer it over hand-rolled OpenSSL:

Terminal window
# On the CA host, with ca-cert.pem and ca-key.pem in ./certs
mantisctl cert generate-server \
--name web-prod-01 \
--sans web-prod-01.example.com \
--output-dir ./certs

Agent certificates need both clientAuth and serverAuth, because Thorax dials the agent in listen mode, plus a SAN matching the hostname Thorax will dial. generate-server sets all three; --sans is required for exactly this reason. An agent used only in poll mode can use generate-client instead.

Install on the agent:

Terminal window
# Paths must match tarsus.toml — /etc/mantis/certs, not /etc/mantis
scp ./certs/web-prod-01-cert.pem agent-server:/etc/mantis/certs/tarsus-cert.pem
scp ./certs/web-prod-01-key.pem agent-server:/etc/mantis/certs/tarsus-key.pem
scp ./certs/ca-cert.pem agent-server:/etc/mantis/certs/ca-cert.pem
ssh agent-server 'systemctl restart tarsus@<instance>'

Using HashiCorp Vault:

Terminal window
# Enable PKI secrets engine
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
# Generate root CA
vault write pki/root/generate/internal \
common_name="Mantis CA" \
ttl=87600h
# Create role for agent certificates
vault write pki/roles/mantis-agent \
allowed_domains="mantis.local" \
allow_subdomains=true \
max_ttl=8760h \
key_type=rsa \
key_bits=2048
# Issue certificate for agent
vault write pki/issue/mantis-agent \
common_name="web-prod-01.mantis.local" \
ttl=8760h

Standard agent certificate template:

FieldValue
CNAgent hostname or identifier
OTeam or tenant name
Key UsageDigital Signature, Key Encipherment
Extended Key UsageClient Authentication, Server Authentication
Subject Alt NameDNS name Thorax dials (listen mode)
Validity365 days

Reserved CNs are rejected at enrollment: mandible (and mandible.<region>) is Mandible’s dispatch identity, and thorax, client and server are reserved as well. Pick agent CNs that do not collide with these.

[tls]
cert_path = "/etc/mantis/certs/tarsus-cert.pem"
key_path = "/etc/mantis/certs/tarsus-key.pem"
# CA certificate for verifying Thorax
ca_cert_path = "/etc/mantis/certs/ca-cert.pem"
Terminal window
# Install certificate files
cp agent.crt /etc/mantis/certs/tarsus-cert.pem
cp agent.key /etc/mantis/certs/tarsus-key.pem
cp ca.crt /etc/mantis/certs/ca-cert.pem
# Set permissions
chmod 644 /etc/mantis/certs/tarsus-cert.pem /etc/mantis/certs/ca-cert.pem
chmod 600 /etc/mantis/certs/tarsus-key.pem
chown root:root /etc/mantis/certs/*.pem /etc/mantis/certs/ca-cert.pem

Using cert-manager (Kubernetes):

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: tarsus-client
spec:
secretName: tarsus-client-tls
duration: 8760h # 1 year
renewBefore: 720h # 30 days before
issuerRef:
name: mantis-ca
kind: ClusterIssuer
commonName: web-prod-01
usages:
- client auth
- digital signature

Certificate revocation options:

MethodApplies toTakes effect
Mantis revocationBoth TLS modesWithin 60s
CRLCaSigned mode, if crl_path is setNext CRL reload

mantisctl cert revoke works in both modes, including for CA-signed agents:

Terminal window
mantisctl cert revoke "<REGISTRATION_UUID>" \
--reason "Certificate compromised"

Thorax re-checks revocation on every gRPC call and caches the answer for 60 seconds, so a revoked agent stops being served within that window. The check fails closed: if Mandible is unreachable, Thorax rejects every call rather than assume the agent is still valid.

Revocation is permanent. A revoked thumbprint can never be registered again, so recovering an agent revoked by mistake means issuing it a new certificate.

For agents you want cut off at the TLS layer — before the application sees them at all — point Thorax at a CRL:

[tls]
auth_mode = "ca_signed"
ca_cert_path = "/etc/mantis/ca/certs/ca.crt"
crl_path = "/etc/mantis/ca/crl" # a PEM CRL, or a directory of them

Only the agent (end-entity) certificate’s revocation status is checked. Requiring a CRL for every issuer in the chain would reject all clients whenever an intermediate’s CRL was missing, which is a worse failure than the one this guards against.

AspectRecommendation
CA key storageHSM or encrypted storage
CA accessLimited, audited access
CA backupSecure offline backup
CA validity10+ years

CA-signed mode provides layered security:

┌─────────────────────────────────────────────────────────────┐
│ Security Layers │
├─────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: TLS encryption │
│ All communication encrypted │
│ │
│ Layer 2: CA validation │
│ Only CA-signed certificates accepted │
│ │
│ Layer 3: Revocation check │
│ Re-checked per gRPC call; fails closed │
│ │
└─────────────────────────────────────────────────────────────┘

Thumbprint verification and registration status are not layers in this mode — a CA-signed certificate bypasses both. They apply only to agents whose certificate the CA did not sign.

Layer 3 fails closed: if Mandible is unreachable, Thorax treats every certificate as revoked and rejects all calls. A Mandible outage therefore presents as fleet-wide certificate has been revoked errors rather than as a connectivity error.

In CA-signed mode, trust is established by the CA signature: any certificate signed by the configured CA is accepted at the TLS layer, and the SHA-256 thumbprint is checked at the application layer. There are no required_org / required_ou subject-field filters — restrict which certificates are issued by controlling your CA’s issuance policy.

For large deployments, use intermediate CAs:

Root CA (offline)
└── Intermediate CA (online)
├── Agent certificates
└── Server certificates

Trust multiple CAs for different environments:

[tls]
# Concatenate multiple CA certificates into a single file
ca_cert_path = "/etc/mantis/certs/combined-ca.crt"

Or concatenate CA certificates:

Terminal window
cat production-ca.crt staging-ca.crt > combined-ca.crt

Problem: certificate verify failed: unable to get local issuer certificate

Diagnosis:

Terminal window
# Verify certificate chain
openssl verify -CAfile /etc/mantis/ca/certs/ca.crt \
/etc/mantis/certs/tarsus-cert.pem
# Check CA file on Thorax
cat /etc/mantis/certs/ca.crt

Solution:

Terminal window
# Ensure CA certificate matches
diff /etc/mantis/ca/certs/ca.crt /path/used/by/thorax/ca.crt
# If intermediate CA, include full chain
cat intermediate.crt root.crt > ca-chain.crt

Problem: Agent rejected after certificate renewal

Solution:

Terminal window
# In CA-signed mode trust derives from the CA signature, so a renewed cert from the same
# CA is accepted without re-registration. If a registration tracks a specific thumbprint,
# re-register the new thumbprint (revoke old, pre-register new):
mantisctl cert revoke "$OLD_REGISTRATION_ID" --reason "certificate renewal"
mantisctl cert pre-register --name "web-prod-01" --thumbprint "$(mantisctl cert thumbprint /etc/mantis/certs/tarsus-cert.pem)"

Problem: CA-signed clients register but stay pending approval

Check:

Terminal window
# Confirm the agent was given a registration token (token-based auto-approval)
journalctl -u tarsus@<instance> | grep -i "registration"
# Otherwise approve it manually
mantisctl cert approve "$REGISTRATION_ID"

A CA-signed agent will not appear in the pending queue at all — it is approved on first connection. Only agents presenting a certificate the CA did not sign reach that queue; approve those from the queue, or issue them a single-use registration token (TARSUS__REGISTRATION__TOKEN).

ErrorCauseSolution
certificate signed by unknown authorityCA not trustedAdd CA to ca_cert_path
certificate has expiredCert expiredRenew certificate
certificate is not valid for any namesWrong CNCheck certificate CN
registration pendingAwaiting approvalUse a token or manually approve
  1. Deploy CA infrastructure
  2. Configure Thorax for CA-signed mode
  3. Issue CA-signed certificates to agents
  4. Update agent configurations
  5. Agents re-connect with new certificates

auth_mode accepts only thumbprint or ca_signed — there is no combined “both” mode. Migration is a cutover: keep Thorax in thumbprint mode until every agent holds a CA-signed certificate, then switch Thorax to ca_signed and restart.

[tls]
# After all agents have CA-signed certs, switch to ca_signed and restart Thorax
auth_mode = "ca_signed"
ca_cert_path = "/etc/mantis/ca/certs/ca.crt"

If issues occur during migration:

# Revert to thumbprint mode
[tls]
auth_mode = "thumbprint"

Existing thumbprint registrations remain valid.

  • Store CA private key in HSM or encrypted storage
  • Limit CA access to authorized personnel
  • Maintain offline backups
  • Audit all certificate issuance
Certificate TypeRecommended Validity
Root CA10-20 years
Intermediate CA5-10 years
Agent certificates1 year

Use tools like:

  • cert-manager (Kubernetes)
  • HashiCorp Vault
  • Step CA
  • ACME protocol
Terminal window
# List client certificates expiring within the threshold (default 30 days)
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.example.com/api/v1/certificates/expiring?days=30"
  • Document CA renewal procedure
  • Test CA renewal in staging
  • Plan for certificate re-issuance
  • Communicate timeline to teams