Skip to main content

SSH Proof of Possession

The SSH PoP method attests agents using an SSH host certificate. The agent proves possession of the host's SSH private key by signing a challenge using a dual-nonce protocol; the Trust Domain Server validates the certificate against a set of trusted SSH CAs.

This method is well-suited for bare-metal servers, VMs, or any Linux host with an SSH host certificate issued by an internal CA.

Attributes available for SVID issuance

AttributeDescription
ssh_pop.typeAttestation type: always host_cert
ssh_pop.key.typeKey algorithm: ed25519, ecdsa-sha2-nistp256, etc.
ssh_pop.key.idKey ID field from the SSH certificate
ssh_pop.serialCertificate serial number
ssh_pop.principalValid principals from the certificate (multi-valued)
ssh_pop.valid.afterCertificate validity start (Unix timestamp)
ssh_pop.valid.beforeCertificate validity end (Unix timestamp)

All attributes have the origin ssh_pop.

Example SPIFFE ID template:

/ssh/{{ssh_pop.principal}}

Supported Key Types

  • Ed25519 (Recommended)
  • ECDSA P-256
  • ECDSA P-384
  • ECDSA P-521
  • RSA, 2048 bit
  • RSA, 4096 bit
  • RSA, 8192 bit

How to Deploy

Step 1 — Update cluster configuration

Add your SSH CA public key(s) to the server config using SSH authorized_keys format. Multiple CAs can be listed.

section: AgentAttestation
schema: v1
spec:
policies:
- name: ssh_policy
requiredAttestors:
- type: ssh_pop
config:
certAuthorities:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... ca@example.com"
canonicalDomain: example.com

Apply it using spirlctl:

spirlctl config set cluster --id <cluster-id> attestation-policy.yaml

Or using Terraform:

resource "spirl_cluster_config" "agent_attestation" {
cluster_id = spirl_cluster.my_cluster.id
sections = {
AgentAttestation = <<-YAML
section: AgentAttestation
schema: v1
spec:
policies:
- name: ssh_policy
requiredAttestors:
- type: ssh_pop
config:
certAuthorities:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... ca@example.com"
canonicalDomain: example.com
YAML
}
}

Once a configuration document passes validation and is stored, the Defakto control plane syncs it to your Trust Domain Servers automatically. No server or agent restart is required.

Server Configuration Reference

FieldRequiredDescription
certAuthoritiesYesList of trusted SSH CA public keys in authorized_keys format. Certificates must be signed by one of these CAs.
canonicalDomainNoIf set, each principal in the certificate must end with this domain suffix (e.g., example.com).

Step 2 — Configure the Agent

Point the agent at the host's SSH private key and certificate.

agent:
auth:
clusterId: c-xxxxxx
attestors:
- type: ssh_pop
config:
hostKeyPath: /etc/ssh/ssh_host_ed25519_key
hostCertPath: /etc/ssh/ssh_host_ed25519_key-cert.pub
note

The SSH host certificate and key must be made available to the agent pod via a hostPath volume mount. If your organization restricts hostPath mounts, the certificate and key can be delivered via a Kubernetes Secret mounted at the configured paths instead.

Agent Configuration Reference

FieldRequiredDescription
hostKeyPathYesPath to the SSH host private key file.
hostCertPathYesPath to the SSH host certificate file (.pub format).

Step 3 — Verify

Server logs — look for these in order:

  1. "Login started with multi-attestation support" — confirms the agent offered providedMethods: ["ssh_pop"]
  2. "Authorization received and verified" — includes agentAttestationAttributes with the certificate identity:
    {
    "msg": "Authorization received and verified",
    "agentAttestationAttributes": [
    "ssh_pop:ssh_pop.key.id=\"server1.example.com\"",
    "ssh_pop:ssh_pop.serial=\"42\"",
    "ssh_pop:ssh_pop.principal=\"webserver-01.example.com\""
    ]
    }
  3. "Connected to agent" — session is fully established

Agent logs — enable debug logging to see "Sending Login" with attestors: ["ssh_pop"]. At the default log level, "Connected to server" confirms the session is live.

Metrics — confirm proofs are succeeding:

spirl_attestation_signer.proof{attestor_type="ssh_pop",outcome="success"}
spirl_attestation_agent.proof{attestor_type="ssh_pop",outcome="success"}

Alert on outcome="failed" to detect CA validation or challenge-response failures.

Common errors:

ErrorLikely cause
no cluster policy authorizes the provided attestorsAgent's ssh_pop method doesn't match any policy — verify the certificate's signing CA appears in certAuthorities
Attestor rejected proof, policy failedCertificate signed by unknown CA, cert is expired, or canonicalDomain doesn't match the certificate's principals
Authorization failed: challenge response failed validationPrivate key at hostKeyPath doesn't match the certificate at hostCertPath

Attestation Flow

The SSH PoP method uses a dual-nonce challenge to prevent replay and relay attacks:

  1. The agent sends the SSH host certificate as an initial proof.
  2. The server validates the certificate (CA chain, expiry, type, principals) and responds with a server-generated nonce.
  3. The agent generates its own agent nonce and computes the combined challenge: SHA256(server_nonce || agent_nonce).
  4. The agent signs the combined challenge with the host private key and sends the signature along with the agent nonce.
  5. The server verifies the signature against the certificate's public key.

Certificate Requirements

The server enforces the following rules during certificate validation:

  • The certificate must be a host certificate (ssh-keygen -t host), not a user certificate.
  • The certificate must be signed by a CA listed in certAuthorities.
  • The certificate must not be expired.
  • The certificate must not include a source-address critical option.
  • If canonicalDomain is set, all principals must match the domain suffix.

Security Considerations

  • Dual-nonce replay protection: The combined nonce (SHA256(server_nonce || agent_nonce)) ensures the signed challenge is unique per attestation attempt. A captured exchange cannot be replayed because the server nonce changes each time.
  • Relay attack mitigation: Because the agent generates and includes its own nonce, a relay attacker who intercepts the server nonce cannot forward a modified challenge and obtain a valid signature — the agent's nonce ties the signature to the original session.
  • Certificate expiry: The Trust Domain Server enforces certificate validity windows. Use short-lived certificates with automated renewal for stronger security.
  • Revocation: To revoke a compromised CA, remove its entry from certAuthorities in the Managed Config and reapply. Certificates signed by that CA will no longer be accepted.

Troubleshooting

Private key not readable — The agent process must have read access to hostKeyPath. Check file permissions; the key is typically root-owned (600).

Certificate signed by unknown CA — The CA public key in hostCertPath must appear in certAuthorities. Verify using ssh-keygen -L -f <cert> to inspect the signing CA fingerprint.

Certificate is a user cert, not a host cert — Confirm the certificate was issued with -h (host cert) flag. ssh-keygen -L -f <cert> shows Type: ssh-ed25519-cert-v01@openssh.com host certificate for a valid host cert.

Principal domain mismatch — If canonicalDomain is set, ensure all principals in the certificate end with that domain. Check with ssh-keygen -L -f <cert> under the Principals field.