← Knowledge base

Caddy

Is PQC enabled? — quick check

macOS / Linux

# 1) No-dependency check — identify this machine first.
uname -a 2>/dev/null || true

# 2) Dependency check — prompt before installing anything.
if ! command -v openssl >/dev/null 2>&1; then
  echo 'OpenSSL was not found. A local PQC proof needs OpenSSL 3.5+.'
  printf 'Install OpenSSL now? [y/N] '
  read answer
  case "$answer" in
    [Yy]*)
      if command -v brew >/dev/null 2>&1; then brew install openssl@3
      elif command -v apt-get >/dev/null 2>&1; then sudo apt-get update && sudo apt-get install -y openssl
      elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y openssl
      elif command -v yum >/dev/null 2>&1; then sudo yum install -y openssl
      else echo 'No supported package manager found. Install OpenSSL 3.5+ and retry.'; exit 1
      fi ;;
    *) echo 'Install OpenSSL 3.5+ and retry for a local PQC proof.'; exit 1 ;;
  esac
fi

OPENSSL=openssl
if command -v brew >/dev/null 2>&1; then
  BREW_OPENSSL="$(brew --prefix openssl@3 2>/dev/null)/bin/openssl"
  [ -x "$BREW_OPENSSL" ] && OPENSSL="$BREW_OPENSSL"
fi

$OPENSSL version
if ! $OPENSSL list -tls-groups 2>/dev/null | grep -qiE 'X25519MLKEM768|MLKEM|Kyber'; then
  echo 'This OpenSSL does not advertise ML-KEM groups. Upgrade to OpenSSL 3.5+ or load oqsprovider, then retry.'
  exit 1
fi
$OPENSSL list -kem-algorithms 2>/dev/null | grep -iE 'mlkem|kyber'   || echo 'no native ML-KEM (try "$OPENSSL list -providers" for oqsprovider)'

# 3) Live handshake against your server. The 'Negotiated TLS1.3 group' line
#    (or 'Server Temp Key' on OpenSSL 3.5) tells you what was negotiated
#    end-to-end — no external service required.
$OPENSSL s_client -connect example.com:443 -tls1_3 -groups X25519MLKEM768   </dev/null 2>&1 |
  grep -E 'Negotiated TLS1\.3 group|Server Temp Key|Cipher is|alert'

Expected when PQC is ON

OpenSSL 3.6.2 7 Apr 2026
  X25519MLKEM768 @ default
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

What you'll see when PQC is OFF

OpenSSL 3.0.13 30 Jan 2024
no native ML-KEM
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server Temp Key: X25519, 253 bits

To enable PQ on your server, see the nginx, Apache, or Caddy KB pages — it's one line: ssl_ecdh_curve / SSLOpenSSLConfCmd Groups / tls.curve_preferences = X25519MLKEM768:X25519:secp384r1.

Windows PowerShell

# 1) No-dependency check — identify this machine first.
[System.Environment]::OSVersion.Version
Get-ComputerInfo | Select-Object OsName,OsVersion,OsBuildNumber

# 2) Dependency check — prompt before installing anything.
function Confirm-Install($Message) {
  $answer = Read-Host "$Message Install now? [y/N]"
  return ($answer -match '^[Yy]')
}

function Get-OpenSSLPath {
  $cmd = Get-Command openssl.exe -ErrorAction SilentlyContinue
  if ($cmd) { return $cmd.Source }

  $programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
  $currentPath = (Get-Location).Path
  $candidateDirs = @(
    $(if ($env:ProgramFiles) { Join-Path (Join-Path $env:ProgramFiles 'OpenSSL-Win64') 'bin' }),
    $(if ($programFilesX86) { Join-Path (Join-Path $programFilesX86 'OpenSSL-Win32') 'bin' }),
    $(if ($env:LOCALAPPDATA) { Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'OpenSSL-Win64') 'bin' }),
    $currentPath,
    $(if ($currentPath) { Join-Path (Join-Path $currentPath 'openssl') 'bin' })
  )
  foreach ($dir in $candidateDirs) {
    if (-not $dir) { continue }
    $candidate = Join-Path $dir 'openssl.exe'
    if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $candidate }
  }
  return $null
}

$openssl = Get-OpenSSLPath
if (-not $openssl) {
  Write-Warning 'OpenSSL 3.5+ was not found. Built-in Windows PowerShell/Schannel cannot prove the PQC named group.'
  if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Install 'Install ShiningLight OpenSSL with winget?')) {
    winget install ShiningLight.OpenSSL.Light
    $openssl = Get-OpenSSLPath
  }
  if (-not $openssl) { 'Install or pre-stage OpenSSL 3.5+ and retry.'; return }
}

& $openssl version
$groups = (& $openssl list -tls-groups 2>$null) -join [Environment]::NewLine
if ($groups -notmatch 'X25519MLKEM768|MLKEM|Kyber') {
  'Local OpenSSL does not advertise ML-KEM groups. Install or pre-stage OpenSSL 3.5+ and retry.'
  return
}
& $openssl list -kem-algorithms | Select-String -Pattern 'mlkem|kyber'

# 3) Live handshake — fully local, no api.checkpqc.app needed.
& $openssl s_client -connect example.com:443 -tls1_3 -groups X25519MLKEM768 2>&1 |
  Select-String -Pattern 'Negotiated TLS1\.3 group|Server Temp Key|Cipher is|alert'

Expected when PQC is ON

OpenSSL 3.6.2 7 Apr 2026
  X25519MLKEM768 @ default
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

What you'll see when PQC is OFF

OpenSSL 1.1.1w  11 Sep 2023
# (no mlkem line)
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
# (no group line — 1.1.1 cannot offer ML-KEM)

Schannel cannot expose the negotiated TLS named group programmatically, so a local OpenSSL binary is the most reliable offline answer on Windows.

Caddy uses Go's crypto/tls. Go 1.23 (Aug 2024) added X25519MLKEM768 and enables it by default in the TLS 1.3 group list. Caddy 2.8+ built against Go 1.23+ will negotiate hybrid PQC out of the box — there is nothing to configure.

1. Confirm your Caddy was built with Go 1.23+

caddy version

Look for the Go runtime in the build banner. If you're on an older binary, upgrade: brew upgrade caddy, the official Debian/RPM repos, or download from caddyserver.com.

2. Verify negotiation

if ! command -v openssl >/dev/null 2>&1; then
  echo 'OpenSSL was not found. Install OpenSSL 3.5+ before running the local proof.'
  printf 'Install OpenSSL now? [y/N] '; read answer
  case "$answer" in [Yy]*) sudo apt-get update && sudo apt-get install -y openssl ;; *) exit 1 ;; esac
fi
openssl s_client -connect example.com:443 -tls1_3 \
  -groups X25519MLKEM768 </dev/null 2>&1 | grep "Cipher is\|alert"

3. Force only PQC (optional, advanced)

Caddy doesn't expose a group-list directive. If you must restrict groups, set GODEBUG=tlsmlkem=1 in the service environment to ensure ML-KEM stays enabled across Go upgrades, and rely on Caddy's defaults.

Caddy's automatic HTTPS already gives you HSTS, OCSP stapling, modern ciphers, and TLS 1.3 first. Combined with Go's PQC defaults, a vanilla Caddy install is hybrid-PQC by default.

Background

Caddy is written in Go and uses Go's standard crypto/tls stack. Since Go 1.23 (Aug 2024), X25519MLKEM768 is in the default curve preferences list and is offered first. Any Caddy build produced with Go 1.23+ negotiates hybrid PQC with no Caddyfile changes whatsoever — there is no provider, no plugin, no rebuild flag.

Operational notes

References

Run the check on your site →