feat: A2A auth — mutual TLS between fleet agents
Implements mutual TLS for secure agent-to-agent communication (#806).
- scripts/gen_fleet_ca.sh: generate fleet CA (4096-bit RSA, 10-year)
- scripts/gen_agent_cert.sh: per-agent cert signed by fleet CA (timmy, allegro, ezra)
- agent/a2a_mtls.py: A2AServer requiring client cert verification (CERT_REQUIRED),
build_server_ssl_context / build_client_ssl_context helpers, server_from_env()
- ansible/roles/fleet_mtls_certs/: distribute CA + per-agent certs to fleet nodes,
write /etc/hermes/a2a.env, notify hermes-a2a service on change
- ansible/fleet_mtls.yml + ansible/inventory/fleet.ini.example: playbook + example inventory
- tests/agent/test_a2a_mtls.py: 11 tests — authorized agent accepted (200/202),
self-signed cert rejected, no-cert rejected, lifecycle, env-var wiring
Fixes #806
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:28:28 -04:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# gen_fleet_ca.sh — Generate the Hermes fleet Certificate Authority.
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# ./scripts/gen_fleet_ca.sh [--out-dir <dir>]
|
|
|
|
|
#
|
|
|
|
|
# Outputs (default: ~/.hermes/pki/ca/):
|
|
|
|
|
# fleet-ca.key — CA private key (chmod 600, keep secret)
|
|
|
|
|
# fleet-ca.crt — CA certificate (distribute to all fleet nodes)
|
|
|
|
|
#
|
|
|
|
|
# The CA is valid for 10 years. Regenerate + redistribute when it expires.
|
|
|
|
|
# Refs #806
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
CA_SUBJECT="/CN=Hermes Fleet CA/O=Hermes/OU=Fleet"
|
|
|
|
|
CA_DAYS=3650 # 10 years
|
|
|
|
|
|
feat: A2A auth — mutual TLS between fleet agents
Implements mTLS for securing agent-to-agent communication in the Hermes
fleet. Fixes #806.
Changes:
- scripts/gen_fleet_ca.sh: generate a self-signed Fleet CA (4096-bit RSA,
10-year validity) that signs all agent certificates
- scripts/gen_agent_cert.sh: generate per-agent certs (Timmy, Allegro,
Ezra) signed by the fleet CA with SAN entries and clientAuth/serverAuth
extended key usage
- agent/mtls.py: new module providing:
- build_server_ssl_context() — TLS_SERVER context with CERT_REQUIRED,
enforces client cert against Fleet CA
- build_client_ssl_context() — TLS_CLIENT context for outbound A2A calls
- MTLSMiddleware — ASGI middleware that rejects unauthenticated requests
to A2A routes (/.well-known/agent-card*, /api/agent-card, /a2a/) with
HTTP 403 when mTLS is enabled
- is_mtls_configured() — checks HERMES_MTLS_CERT/KEY/CA env vars
- hermes_cli/web_server.py: wire MTLSMiddleware into the FastAPI app;
pass SSL context to uvicorn when HERMES_MTLS_* env vars are set so
the server runs TLS with mandatory client cert verification
- ansible/roles/hermes_mtls/: Ansible role to distribute Fleet CA cert,
agent cert, and agent key to fleet nodes; writes an env file with
HERMES_MTLS_* vars and restarts the hermes-gateway service
- ansible/fleet_mtls.yml: fleet-wide playbook referencing the role for
Timmy, Allegro, and Ezra nodes
- tests/test_mtls.py: 15 tests covering is_mtls_configured, SSL context
creation with real cryptography-generated certs, and MTLSMiddleware
(unauthorized agent rejected → 403, authorized agent accepted → 200)
mTLS is opt-in: set HERMES_MTLS_CERT, HERMES_MTLS_KEY, and HERMES_MTLS_CA
to enable. When unset, the server behaves exactly as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 18:02:59 -04:00
|
|
|
# ---------------------------------------------------------------------------
|
feat: A2A auth — mutual TLS between fleet agents
Implements mutual TLS for secure agent-to-agent communication (#806).
- scripts/gen_fleet_ca.sh: generate fleet CA (4096-bit RSA, 10-year)
- scripts/gen_agent_cert.sh: per-agent cert signed by fleet CA (timmy, allegro, ezra)
- agent/a2a_mtls.py: A2AServer requiring client cert verification (CERT_REQUIRED),
build_server_ssl_context / build_client_ssl_context helpers, server_from_env()
- ansible/roles/fleet_mtls_certs/: distribute CA + per-agent certs to fleet nodes,
write /etc/hermes/a2a.env, notify hermes-a2a service on change
- ansible/fleet_mtls.yml + ansible/inventory/fleet.ini.example: playbook + example inventory
- tests/agent/test_a2a_mtls.py: 11 tests — authorized agent accepted (200/202),
self-signed cert rejected, no-cert rejected, lifecycle, env-var wiring
Fixes #806
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:28:28 -04:00
|
|
|
# Parse args
|
feat: A2A auth — mutual TLS between fleet agents
Implements mTLS for securing agent-to-agent communication in the Hermes
fleet. Fixes #806.
Changes:
- scripts/gen_fleet_ca.sh: generate a self-signed Fleet CA (4096-bit RSA,
10-year validity) that signs all agent certificates
- scripts/gen_agent_cert.sh: generate per-agent certs (Timmy, Allegro,
Ezra) signed by the fleet CA with SAN entries and clientAuth/serverAuth
extended key usage
- agent/mtls.py: new module providing:
- build_server_ssl_context() — TLS_SERVER context with CERT_REQUIRED,
enforces client cert against Fleet CA
- build_client_ssl_context() — TLS_CLIENT context for outbound A2A calls
- MTLSMiddleware — ASGI middleware that rejects unauthenticated requests
to A2A routes (/.well-known/agent-card*, /api/agent-card, /a2a/) with
HTTP 403 when mTLS is enabled
- is_mtls_configured() — checks HERMES_MTLS_CERT/KEY/CA env vars
- hermes_cli/web_server.py: wire MTLSMiddleware into the FastAPI app;
pass SSL context to uvicorn when HERMES_MTLS_* env vars are set so
the server runs TLS with mandatory client cert verification
- ansible/roles/hermes_mtls/: Ansible role to distribute Fleet CA cert,
agent cert, and agent key to fleet nodes; writes an env file with
HERMES_MTLS_* vars and restarts the hermes-gateway service
- ansible/fleet_mtls.yml: fleet-wide playbook referencing the role for
Timmy, Allegro, and Ezra nodes
- tests/test_mtls.py: 15 tests covering is_mtls_configured, SSL context
creation with real cryptography-generated certs, and MTLSMiddleware
(unauthorized agent rejected → 403, authorized agent accepted → 200)
mTLS is opt-in: set HERMES_MTLS_CERT, HERMES_MTLS_KEY, and HERMES_MTLS_CA
to enable. When unset, the server behaves exactly as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 18:02:59 -04:00
|
|
|
# ---------------------------------------------------------------------------
|
feat: A2A auth — mutual TLS between fleet agents
Implements mutual TLS for secure agent-to-agent communication (#806).
- scripts/gen_fleet_ca.sh: generate fleet CA (4096-bit RSA, 10-year)
- scripts/gen_agent_cert.sh: per-agent cert signed by fleet CA (timmy, allegro, ezra)
- agent/a2a_mtls.py: A2AServer requiring client cert verification (CERT_REQUIRED),
build_server_ssl_context / build_client_ssl_context helpers, server_from_env()
- ansible/roles/fleet_mtls_certs/: distribute CA + per-agent certs to fleet nodes,
write /etc/hermes/a2a.env, notify hermes-a2a service on change
- ansible/fleet_mtls.yml + ansible/inventory/fleet.ini.example: playbook + example inventory
- tests/agent/test_a2a_mtls.py: 11 tests — authorized agent accepted (200/202),
self-signed cert rejected, no-cert rejected, lifecycle, env-var wiring
Fixes #806
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:28:28 -04:00
|
|
|
OUT_DIR="${HOME}/.hermes/pki/ca"
|
|
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
|
|
|
|
-h|--help)
|
|
|
|
|
echo "Usage: $0 [--out-dir <dir>]"
|
|
|
|
|
exit 0
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo "Unknown option: $1" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
feat: A2A auth — mutual TLS between fleet agents
Implements mTLS for securing agent-to-agent communication in the Hermes
fleet. Fixes #806.
Changes:
- scripts/gen_fleet_ca.sh: generate a self-signed Fleet CA (4096-bit RSA,
10-year validity) that signs all agent certificates
- scripts/gen_agent_cert.sh: generate per-agent certs (Timmy, Allegro,
Ezra) signed by the fleet CA with SAN entries and clientAuth/serverAuth
extended key usage
- agent/mtls.py: new module providing:
- build_server_ssl_context() — TLS_SERVER context with CERT_REQUIRED,
enforces client cert against Fleet CA
- build_client_ssl_context() — TLS_CLIENT context for outbound A2A calls
- MTLSMiddleware — ASGI middleware that rejects unauthenticated requests
to A2A routes (/.well-known/agent-card*, /api/agent-card, /a2a/) with
HTTP 403 when mTLS is enabled
- is_mtls_configured() — checks HERMES_MTLS_CERT/KEY/CA env vars
- hermes_cli/web_server.py: wire MTLSMiddleware into the FastAPI app;
pass SSL context to uvicorn when HERMES_MTLS_* env vars are set so
the server runs TLS with mandatory client cert verification
- ansible/roles/hermes_mtls/: Ansible role to distribute Fleet CA cert,
agent cert, and agent key to fleet nodes; writes an env file with
HERMES_MTLS_* vars and restarts the hermes-gateway service
- ansible/fleet_mtls.yml: fleet-wide playbook referencing the role for
Timmy, Allegro, and Ezra nodes
- tests/test_mtls.py: 15 tests covering is_mtls_configured, SSL context
creation with real cryptography-generated certs, and MTLSMiddleware
(unauthorized agent rejected → 403, authorized agent accepted → 200)
mTLS is opt-in: set HERMES_MTLS_CERT, HERMES_MTLS_KEY, and HERMES_MTLS_CA
to enable. When unset, the server behaves exactly as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 18:02:59 -04:00
|
|
|
# ---------------------------------------------------------------------------
|
feat: A2A auth — mutual TLS between fleet agents
Implements mutual TLS for secure agent-to-agent communication (#806).
- scripts/gen_fleet_ca.sh: generate fleet CA (4096-bit RSA, 10-year)
- scripts/gen_agent_cert.sh: per-agent cert signed by fleet CA (timmy, allegro, ezra)
- agent/a2a_mtls.py: A2AServer requiring client cert verification (CERT_REQUIRED),
build_server_ssl_context / build_client_ssl_context helpers, server_from_env()
- ansible/roles/fleet_mtls_certs/: distribute CA + per-agent certs to fleet nodes,
write /etc/hermes/a2a.env, notify hermes-a2a service on change
- ansible/fleet_mtls.yml + ansible/inventory/fleet.ini.example: playbook + example inventory
- tests/agent/test_a2a_mtls.py: 11 tests — authorized agent accepted (200/202),
self-signed cert rejected, no-cert rejected, lifecycle, env-var wiring
Fixes #806
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:28:28 -04:00
|
|
|
# Prereq check
|
feat: A2A auth — mutual TLS between fleet agents
Implements mTLS for securing agent-to-agent communication in the Hermes
fleet. Fixes #806.
Changes:
- scripts/gen_fleet_ca.sh: generate a self-signed Fleet CA (4096-bit RSA,
10-year validity) that signs all agent certificates
- scripts/gen_agent_cert.sh: generate per-agent certs (Timmy, Allegro,
Ezra) signed by the fleet CA with SAN entries and clientAuth/serverAuth
extended key usage
- agent/mtls.py: new module providing:
- build_server_ssl_context() — TLS_SERVER context with CERT_REQUIRED,
enforces client cert against Fleet CA
- build_client_ssl_context() — TLS_CLIENT context for outbound A2A calls
- MTLSMiddleware — ASGI middleware that rejects unauthenticated requests
to A2A routes (/.well-known/agent-card*, /api/agent-card, /a2a/) with
HTTP 403 when mTLS is enabled
- is_mtls_configured() — checks HERMES_MTLS_CERT/KEY/CA env vars
- hermes_cli/web_server.py: wire MTLSMiddleware into the FastAPI app;
pass SSL context to uvicorn when HERMES_MTLS_* env vars are set so
the server runs TLS with mandatory client cert verification
- ansible/roles/hermes_mtls/: Ansible role to distribute Fleet CA cert,
agent cert, and agent key to fleet nodes; writes an env file with
HERMES_MTLS_* vars and restarts the hermes-gateway service
- ansible/fleet_mtls.yml: fleet-wide playbook referencing the role for
Timmy, Allegro, and Ezra nodes
- tests/test_mtls.py: 15 tests covering is_mtls_configured, SSL context
creation with real cryptography-generated certs, and MTLSMiddleware
(unauthorized agent rejected → 403, authorized agent accepted → 200)
mTLS is opt-in: set HERMES_MTLS_CERT, HERMES_MTLS_KEY, and HERMES_MTLS_CA
to enable. When unset, the server behaves exactly as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 18:02:59 -04:00
|
|
|
# ---------------------------------------------------------------------------
|
feat: A2A auth — mutual TLS between fleet agents
Implements mutual TLS for secure agent-to-agent communication (#806).
- scripts/gen_fleet_ca.sh: generate fleet CA (4096-bit RSA, 10-year)
- scripts/gen_agent_cert.sh: per-agent cert signed by fleet CA (timmy, allegro, ezra)
- agent/a2a_mtls.py: A2AServer requiring client cert verification (CERT_REQUIRED),
build_server_ssl_context / build_client_ssl_context helpers, server_from_env()
- ansible/roles/fleet_mtls_certs/: distribute CA + per-agent certs to fleet nodes,
write /etc/hermes/a2a.env, notify hermes-a2a service on change
- ansible/fleet_mtls.yml + ansible/inventory/fleet.ini.example: playbook + example inventory
- tests/agent/test_a2a_mtls.py: 11 tests — authorized agent accepted (200/202),
self-signed cert rejected, no-cert rejected, lifecycle, env-var wiring
Fixes #806
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:28:28 -04:00
|
|
|
if ! command -v openssl &>/dev/null; then
|
|
|
|
|
echo "ERROR: openssl not found. Install OpenSSL and re-run." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
|
chmod 700 "$OUT_DIR"
|
|
|
|
|
|
|
|
|
|
CA_KEY="$OUT_DIR/fleet-ca.key"
|
|
|
|
|
CA_CRT="$OUT_DIR/fleet-ca.crt"
|
|
|
|
|
|
|
|
|
|
if [[ -f "$CA_KEY" || -f "$CA_CRT" ]]; then
|
|
|
|
|
echo "Fleet CA already exists in $OUT_DIR"
|
|
|
|
|
echo " $CA_KEY"
|
|
|
|
|
echo " $CA_CRT"
|
|
|
|
|
echo "Delete them manually if you want to regenerate."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Generating fleet CA in $OUT_DIR ..."
|
|
|
|
|
|
|
|
|
|
# Generate 4096-bit RSA key for the CA
|
|
|
|
|
openssl genrsa -out "$CA_KEY" 4096 2>/dev/null
|
|
|
|
|
chmod 600 "$CA_KEY"
|
|
|
|
|
|
|
|
|
|
# Self-sign the CA certificate
|
|
|
|
|
openssl req -new -x509 \
|
|
|
|
|
-key "$CA_KEY" \
|
|
|
|
|
-out "$CA_CRT" \
|
|
|
|
|
-days "$CA_DAYS" \
|
|
|
|
|
-subj "$CA_SUBJECT" \
|
|
|
|
|
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
|
|
|
|
|
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
|
|
|
-addext "subjectKeyIdentifier=hash" 2>/dev/null
|
|
|
|
|
|
|
|
|
|
chmod 644 "$CA_CRT"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Fleet CA generated successfully:"
|
|
|
|
|
echo " Private key : $CA_KEY (keep secret)"
|
|
|
|
|
echo " Certificate : $CA_CRT (distribute to all fleet nodes)"
|
|
|
|
|
echo ""
|
|
|
|
|
openssl x509 -in "$CA_CRT" -noout -subject -dates
|