Privasys
Confidential Containers

Caddy RA-TLS Module

A Caddy TLS issuance module that produces RA-TLS certificates for Confidential Computing (Intel TDX, AMD SEV-SNP).

The RA-TLS for Caddy is a Caddy tls.issuance module (ra_tls) that produces RA-TLS certificates for Confidential Computing. It turns any Caddy instance running inside a Confidential VM into an attested HTTPS server — no custom client-side code required.

Repository: github.com/Privasys/ra-tls-caddy

Supported Backends

BackendTEEStatus
tdxIntel TDXSupported
sev-snpAMD SEV-SNPPlanned

The hardware-specific logic is abstracted behind the Attester interface (attester.go). Each backend lives in its own file (e.g. attester_tdx.go) and registers itself via init().

Why Caddy + Confidential VMs

BenefitDescription
Ease of deploymentCaddy is a mature, production-grade web server. Adding RA-TLS is a single module — no code changes to your application.
Unmodified LinuxTDX/SEV protect an entire VM, so your application runs as a normal Linux process. No SGX SDK, no special build toolchain.
Standard CaddyfileConfigure RA-TLS with a few lines in the Caddyfile, just like any other TLS issuer.
Reverse proxyCaddy can reverse-proxy to any backend service running in the same Confidential VM, adding RA-TLS attestation to existing applications.

Extended Certificate

The module serves an X.509 certificate extended with an attribute containing the TEE quote. For Intel TDX the attestation evidence is embedded in extension OID 1.2.840.113741.1.5.5.1.6:

X509v3 extensions:
    ...
    1.2.840.113741.1.5.5.1.6:
        <hex dump of the TDX quote — ~8000 bytes of attestation evidence>

See RA-TLS for details on how RA-TLS certificates work across both Enclave OS and Caddy.

Architecture

┌──────────────────────────────────────────────┐
│          Confidential VM (TDX / SEV)         │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │           Caddy Server                 │  │
│  │                                        │  │
│  │  ┌──────────────┐  ┌───────────────┐   │  │
│  │  │ RA-TLS       │  │ Reverse Proxy │   │  │
│  │  │ Issuance     │  │ / File Server │   │  │
│  │  │ Module       │  │ / Application │   │  │
│  │  └──────┬───────┘  └───────────────┘   │  │
│  │         │                              │  │
│  │         ▼                              │  │
│  │  ┌──────────────────────┐              │  │
│  │  │ configfs-tsm         │              │  │
│  │  │ /sys/kernel/config/  │              │  │
│  │  │   tsm/report         │              │  │
│  │  └──────────────────────┘              │  │
│  └────────────────────────────────────────┘  │
│                                              │
│        Hardware Memory Encryption            │
└──────────────────────────────────────────────┘

The TDX backend uses the Linux kernel's configfs-tsm interface (/sys/kernel/config/tsm/report) to request quotes — no special SDK or library needed.

How It Works

The module supports two attestation paths:

Deterministic (Issue Path)

  1. Key Generation — An ECDSA P-256 key pair is generated inside the TEE.
  2. AttestationReportData = SHA-512( SHA-256(DER public key) || creation_time ), where creation_time is NotBefore truncated to 1-minute precision ("2006-01-02T15:04Z"). The quote is obtained from the configured backend.
  3. Certificate — An X.509 certificate is signed by a user-provided intermediary CA (private PKI), embedding the attestation evidence in a backend-specific extension OID. The PEM output includes the full chain (leaf + CA cert).

Certificates are cached by CertMagic and auto-renewed. A verifier reproduces the ReportData from the certificate alone.

Challenge-Response (GetCertificate Path)

When a TLS client sends an RA-TLS challenge in its ClientHello (extension 0xffbb):

  1. Ephemeral Key — A fresh ECDSA P-256 key pair is generated.
  2. AttestationReportData = SHA-512( SHA-256(DER public key) || nonce ), binding the quote to the client's challenge.
  3. Certificate — A very short-lived (5 min) certificate is signed by the same CA, with the quote embedded.

This certificate is not cached — each challenge produces a unique response.

Note: Challenge-response attestation requires the Privasys/go fork (initial commit) which adds ClientHelloInfo.RATLSChallenge to crypto/tls. The code references hello.RATLSChallenge directly and will not compile with standard Go. An upstream PR is open at golang/go#77714.

Requirements

  • Linux host running inside a Confidential VM.
  • Backend-specific support:
    • tdx — Kernel configfs-tsm (/sys/kernel/config/tsm/report).
  • An intermediary CA certificate and private key (private PKI).
  • Privasys/go fork (ratls branch — initial commit) and xcaddy.

Building

First, build the Go fork:

# Install official Go (needed as bootstrap compiler)
wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
rm go1.24.0.linux-amd64.tar.gz

# Clone and build the Privasys Go fork
git clone -b ratls https://github.com/Privasys/go.git ~/go-ratls
export GOROOT_BOOTSTRAP=/usr/local/go
cd ~/go-ratls/src && ./make.bash
export GOROOT=~/go-ratls
export PATH=$GOROOT/bin:$HOME/go/bin:$PATH

Then build Caddy with the RA-TLS module:

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

git clone https://github.com/Privasys/ra-tls-caddy.git
cd ra-tls-caddy/src
xcaddy build --with github.com/Privasys/ra-tls-caddy=.

Note: The =. suffix tells xcaddy to use the local directory as the module source. The import path before = must match the module directive in go.mod. If you are working from a fork, update go.mod accordingly.

For full deployment guides on specific cloud providers, see the Guides section.

Configuration

Caddyfile

example.com {
    tls {
        issuer ra_tls {
            backend tdx
            ca_cert /path/to/intermediate-ca.crt
            ca_key  /path/to/intermediate-ca.key
        }
    }
    respond "Hello from a Confidential VM!"
}

JSON Configuration

{
  "apps": {
    "tls": {
      "automation": {
        "policies": [
          {
            "subjects": ["example.com"],
            "issuers": [
              {
                "module": "ra_tls",
                "backend": "tdx",
                "ca_cert_path": "/path/to/intermediate-ca.crt",
                "ca_key_path": "/path/to/intermediate-ca.key"
              }
            ]
          }
        ]
      }
    }
  }
}

Inspecting the Certificate

Once Caddy is running, inspect the RA-TLS certificate with standard tools:

# Retrieve and display the full certificate
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text

To save the certificate for programmatic verification:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -outform PEM > ratls-cert.pem

openssl asn1parse -in ratls-cert.pem

Key Sensitivity

The ECDSA private key is generated inside the TEE and protected by hardware memory encryption at runtime. However, CertMagic will PEM-encode and persist it via its storage backend. To prevent writing it to unencrypted disk:

  • Use an encrypted filesystem or volume for Caddy's data directory.
  • Or configure a secrets-manager storage module.

Verification

A relying party verifying an RA-TLS certificate should:

Deterministic Path

  1. Validate the certificate chain back to the trusted root CA.
  2. Extract the attestation evidence from the backend-specific extension OID (e.g. 1.2.840.113741.1.5.5.1.6 for TDX).
  3. Verify the evidence against the hardware vendor's attestation infrastructure.
  4. Read the certificate's NotBefore field, format it as "2006-01-02T15:04Z" (UTC, minute precision).
  5. Compute SHA-512( SHA-256(DER public key) || formatted_time_string ) and confirm it matches the quote's ReportData.
  6. Check the quote's measurement registers against expected values.

Challenge-Response Path

  1. Validate the certificate chain back to the trusted root CA.
  2. Extract the attestation evidence from the backend-specific extension OID.
  3. Verify the evidence against the hardware vendor's attestation infrastructure.
  4. Compute SHA-512( SHA-256(DER public key) || original_nonce ) using the nonce sent in the ClientHello.
  5. Confirm the result matches the quote's ReportData — this proves freshness.
  6. Check the quote's measurement registers against expected values.

See RA-TLS Clients for verification libraries in Python, Go, Rust, TypeScript, and C#.

Adding a New Backend

  1. Create src/attester_<name>.go implementing the Attester interface.
  2. Register it in an init() function: RegisterAttester("<name>", func() Attester { return new(MyAttester) })
  3. The new backend is immediately available via backend <name> in the Caddyfile.
Edit on GitHub