Regulators in the EU and UK are starting to require age checks before people can use large parts of the internet. The usual way to comply is to collect a government ID or a face scan, which means handing your identity to every service that wants to confirm one fact about you: whether you're old enough. That trades privacy for compliance, and it's becoming the default.
age-mtls takes a different route. You verify your age once with a trusted authority and receive a client certificate that proves a single attribute (over 18, human, or automated) without revealing who you are. Sites check the certificate instead of your ID. The certificate carries the claim, and nobody downstream learns your name, your birth date, or anything else about you.
The goal is to make this an operating-system primitive rather than a per-website feature. If age proof lives in the OS, any app or browser can request it through one trusted path, the user approves it once, and no individual site ever touches an identity document. That's what separates a privacy-preserving platform capability from yet another login wall, and it's why the OS-level work matters.
This is early. The cryptographic flow already works: a certificate authority issues certificates that carry an access level, a verifier tracks them, and a TLS-protected service grants access based on the level. Today the three services run on Kubernetes and use a mock authentication step in place of a real identity provider. Moving the verification flow down to the OS level is the active work.
There are three services, each in its own directory under
mtls-project-microservices/:
Issues and signs client certificates. It loads the CA certificate and key
(mounted from a Kubernetes secret at /etc/ca), generates a client certificate
with rcgen, assigns it a serial number, and registers that serial with the
verifier before returning the certificate and private key to the caller.
POST /issue: issue a new certificateGET /health: health check
Stores certificate records in Redis and answers verification queries. Each
record holds the user ID, access level, issue and expiry timestamps, and a
revoked flag. Records are keyed by cert:{serial} and expire automatically when
the certificate does.
POST /register: register a certificateGET /verify/:serial: verify a certificate and return its access levelPOST /revoke/:serial: revoke a certificateGET /health: health check
Serves over HTTPS using rustls, with the server certificate and key mounted at
/etc/tls. It hands out certificates through a mock authentication page and
exposes the protected routes.
/: public landing page/auth: request a certificate (mock BankID flow)/protected: protected area/admin: admin area (intended for the highest access level)/health: health check
Certificates carry one of three levels, which stand in for different verified attributes:
| Level | Name | Meaning |
|---|---|---|
| 1 | Bot | Automated systems and APIs |
| 2 | Human | A verified human user |
| 3 | Adult | An age-verified user (18+) |
This mirrors real situations like proving you're a real person, proving you're over 18, or proving a specific attribute without handing over your full identity.
1. A user visits the website and goes to /auth
2. They pick an access level and authenticate (mock BankID)
3. The CA Service issues a certificate, signs it, and registers the serial
with the Verifier (serial -> access level)
4. The user receives the certificate and private key
5. They reconnect with the certificate; the mTLS handshake runs and the
Verifier confirms the certificate is valid and reports its level
- Language: Rust, on the Tokio async runtime
- Web framework: Axum
- TLS: rustls (via
axum-server) - Certificate generation: rcgen
- Storage: Redis, for certificate records
- Containers: Docker, multi-stage builds
- Orchestration: Kubernetes
mtls-project-microservices/
├── ca-service/ # Certificate authority
├── verifier-service/ # Certificate verification + Redis
├── protected-website/ # mTLS-protected website
├── infrastructure/ # Namespace and Redis manifests
├── scripts/ # generate-ca, deploy, cleanup, test
├── config/ # Environment configuration
└── docs/ # Architecture and deployment notes
Each service is self-contained: its own Cargo workspace, Dockerfile, Kubernetes
manifests, and values.yaml. There's also a mtls-project-monolith/ variant
that keeps the same three services in a single combined project.
You'll need Docker, a Kubernetes cluster (minikube, kind, or self-hosted), kubectl, and Rust for local development.
cd mtls-project-microservices
# Generate the CA and server certificates
make setup
# Deploy everything to Kubernetes
make deploy
# Run the integration test
make testTo deploy the services individually instead:
kubectl apply -f infrastructure/k8s/
kubectl apply -f ca-service/k8s/
kubectl apply -f verifier-service/k8s/
kubectl apply -f protected-website/k8s/Run a single service:
cd ca-service
cargo runIssue a certificate against a running CA Service:
curl -X POST http://localhost:8080/issue \
-H 'Content-Type: application/json' \
-d '{"user_id": "test", "access_level": 3, "validity_days": 30}'Or build all three at once with make build-local. The Makefile also has
shortcuts for issuing certificates at each level (make issue-bot,
make issue-human, make issue-adult), tailing logs, and opening a Redis CLI.
MIT licensed. Early-stage project with a working proof of concept.