Configure mTLS
Set up mutual TLS for client certificate authentication between container runtimes and Angos.
Prerequisites
- Angos instance that terminates TLS itself (not behind a TLS-terminating proxy)
- Server certificate and private key
- Client CA certificate (from your internal PKI)
- Client certificate and private key
Configure Angos
Step 1: Prepare Certificates
Organize your certificates:
/tls/
├── server-certificate.pem # Server cert (can include chain)
├── server-private-key.pem # Server private key
└── client-ca-bundle.pem # Trusted client CAs
Step 2: Update Configuration
Add TLS configuration to config.toml:
[server]
bind_address = "0.0.0.0"
port = 8000
[server.tls]
server_certificate_bundle = "/tls/server-certificate.pem"
server_private_key = "/tls/server-private-key.pem"
client_ca_bundle = "/tls/client-ca-bundle.pem"
# Controls how the server treats client certificates when client_ca_bundle is set:
# "optional": clients may connect with or without a cert (cert identity is used in policy when present).
# "required": the TLS handshake fails unless the client presents a cert signed by client_ca_bundle.
client_auth = "required"
client_auth modes
client_auth is only meaningful when client_ca_bundle is set. When no client_ca_bundle is configured, no client authentication occurs and client_auth is ignored.
| Mode | Behavior |
|---|---|
"optional" (default) | Clients may connect with or without a certificate. Certificate identity is available in access policy when a cert is presented. Use this when mTLS is one of several accepted auth methods. |
"required" | The TLS handshake is rejected unless the client presents a certificate signed by a CA in client_ca_bundle. Use this when mTLS is the only acceptable authentication method. |
client_auth = "required" requires client_ca_bundle to be set; the configuration is rejected at startup otherwise. When client_auth is omitted, it defaults to "optional", so a config that sets only client_ca_bundle gets optional client authentication.
Step 3: Restart the Registry
./angos -c config.toml server
Configure Container Runtimes
Docker
Create the certificate directory:
mkdir -p /etc/docker/certs.d/registry.example.com
Copy certificates:
cp client-certificate.pem /etc/docker/certs.d/registry.example.com/client.cert
cp client-private-key.pem /etc/docker/certs.d/registry.example.com/client.key
cp ca-certificate.pem /etc/docker/certs.d/registry.example.com/ca.crt
Restart Docker:
systemctl restart docker
Podman
Create the certificate directory:
mkdir -p /etc/containers/certs.d/registry.example.com
Copy certificates:
cp client-certificate.pem /etc/containers/certs.d/registry.example.com/client.cert
cp client-private-key.pem /etc/containers/certs.d/registry.example.com/client.key
cp ca-certificate.pem /etc/containers/certs.d/registry.example.com/ca.crt
containerd
Create the certificate directory:
mkdir -p /etc/containerd/certs.d/registry.example.com
Copy certificates:
cp client-certificate.pem /etc/containerd/certs.d/registry.example.com/client.cert
cp client-private-key.pem /etc/containerd/certs.d/registry.example.com/client.key
cp ca-certificate.pem /etc/containerd/certs.d/registry.example.com/ca.crt
Configure containerd 2.x (/etc/containerd/config.toml):
version = 3
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/containerd/certs.d"
For containerd 1.x:
version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
Restart containerd:
systemctl restart containerd
Access Control with Certificates
Use certificate attributes in access policies:
[global.access_policy]
default = "deny"
rules = [
# Allow clients with specific organization
"identity.certificate.organizations.contains('Infrastructure')",
# Allow specific common name
"'build-server' in identity.certificate.common_names",
# Combine with other auth methods
"identity.username == 'admin'"
]
Available certificate variables:
identity.certificate.common_names- List of CNsidentity.certificate.organizations- List of Organizations
Verification
Test with curl:
curl --cert client.pem --key client-key.pem \
--cacert ca.pem \
https://registry.example.com:8000/v2/
Test with Docker:
docker pull registry.example.com/test/alpine:latest
mTLS Scenarios
Scenario 1: Certificate Required
All clients must present a valid certificate. Use client_auth = "required" to enforce this at the TLS layer; the connection is rejected before any application logic runs:
[server.tls]
server_certificate_bundle = "/tls/server-certificate.pem"
server_private_key = "/tls/server-private-key.pem"
client_ca_bundle = "/tls/client-ca-bundle.pem"
client_auth = "required"
You can additionally enforce specific certificate attributes in access policy:
[global.access_policy]
default = "deny"
rules = [
"size(identity.certificate.common_names) > 0"
]
Scenario 2: Certificate Optional
Certificate enhances access but isn't required:
[global.access_policy]
default = "deny"
rules = [
# Certificate grants full access
"identity.certificate.organizations.contains('DevOps')",
# Basic auth for read-only
"identity.username != '' && request.action.startsWith('get-')"
]
Scenario 3: Different Orgs, Different Access
# Developers can push to dev repo
[repository."dev".access_policy]
default = "deny"
rules = [
"identity.certificate.organizations.contains('Developers')"
]
# Only platform team can push to production
[repository."prod".access_policy]
default = "deny"
rules = [
"identity.certificate.organizations.contains('Platform')"
]
Troubleshooting
Certificate rejected:
- Verify the client certificate is signed by a CA in
client_ca_bundle - Check certificate expiration:
openssl x509 -in client.pem -noout -dates - Verify the chain is complete
Connection refused:
- Ensure TLS is configured (not running in insecure mode)
- Check firewall rules
- Verify the server certificate is valid for the hostname
Debug logging:
RUST_LOG=angos::auth=debug ./angos server
Next Steps
- Set Up Access Control for fine-grained policies
- Configure Webhook Authorization for external policy decisions