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 = 5000
[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"
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_allow = false
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:5000/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:
[global.access_policy]
default_allow = false
rules = [
"size(identity.certificate.common_names) > 0"
]
Scenario 2: Certificate Optional
Certificate enhances access but isn't required:
[global.access_policy]
default_allow = false
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_allow = false
rules = [
"identity.certificate.organizations.contains('Developers')"
]
# Only platform team can push to production
[repository."prod".access_policy]
default_allow = false
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::command::server::auth=debug ./angos server
Next Steps
- Set Up Access Control for fine-grained policies
- Configure Webhook Authorization for external policy decisions