Skip to main content

Set Up Access Control

Configure CEL-based access control policies for fine-grained authorization.

Prerequisites

  • Angos running
  • Optional: Authentication configured (basic auth, mTLS, or OIDC)

Basic Concepts

Access control uses CEL (Common Expression Language) to evaluate rules:

  • default_allow = true: Deny if any rule returns false
  • default_allow = false: Allow if any rule returns true

Policies can be configured globally or per-repository.


Global Policy

Apply policies to all repositories:

[global.access_policy]
default_allow = false
rules = [
"identity.username != null" # Require authentication
]

Repository Policy

Override or supplement global policies for specific repositories:

[repository."production".access_policy]
default_allow = false
rules = [
"identity.username == 'admin'",
"identity.certificate.organizations.contains('Platform')"
]

Common Patterns

Require Authentication

[global.access_policy]
default_allow = false
rules = [
"identity.username != null"
]

Read-Only for Everyone, Write for Admins

[global.access_policy]
default_allow = false
rules = [
# Anyone can read
"request.action.startsWith('get-') || request.action == 'list-tags'",

# Only admin can write
"identity.username == 'admin'"
]

IP-Based Access

rules = [
# Allow from internal network
"identity.client_ip.startsWith('10.0.')",

# Allow authenticated users from anywhere
"identity.username != null"
]

Certificate Organization

rules = [
"identity.certificate.organizations.contains('Engineering')"
]

OIDC Claims

rules = [
'''identity.oidc != null &&
identity.oidc.claims["email"].endsWith("@company.com")'''
]

Action-Based Policies

Control access to specific operations:

Push/Pull Separation

rules = [
# Everyone can pull
"request.action == 'get-blob' || request.action == 'get-manifest'",

# Only deployers can push
"identity.username == 'deployer' && request.action.startsWith('put-')"
]

Delete Restriction

rules = [
# Allow normal operations
"identity.username != null && !request.action.startsWith('delete-')",

# Only admin can delete
"identity.username == 'admin'"
]

Health and Metrics

rules = [
# Allow unauthenticated health checks
"request.action == 'healthz'",

# Require auth for everything else
"identity.username != null"
]

Web UI Access

Control access to UI-specific actions:

[global.access_policy]
default_allow = false
rules = [
# Allow UI assets to load
"request.action == 'ui-asset' || request.action == 'ui-config'",

# Require auth for browsing
"identity.username != null && request.action.startsWith('list-')",

# Normal registry operations
"identity.username != null"
]

Mixed Authentication

Combine different authentication methods:

[global.access_policy]
default_allow = false
rules = [
# Basic auth admin
"identity.username == 'admin'",

# mTLS with specific organization
"identity.certificate.organizations.contains('DevOps')",

# GitHub Actions from organization
'''identity.oidc != null &&
identity.oidc.provider_name == "github-actions" &&
identity.oidc.claims["repository"].startsWith("myorg/")''',

# Corporate OIDC users can pull
'''identity.oidc != null &&
identity.oidc.provider_name == "corporate" &&
request.action.startsWith("get-")'''
]

Multi-Repository Setup

# Global baseline
[global.access_policy]
default_allow = false
rules = [
"request.action == 'healthz'" # Always allow health checks
]

# Public read-only repo
[repository."public".access_policy]
default_allow = false
rules = [
"request.action.startsWith('get-') || request.action == 'list-tags'"
]

# Development: team access
[repository."dev".access_policy]
default_allow = false
rules = [
"identity.certificate.organizations.contains('Engineering')"
]

# Production: restricted access
[repository."prod".access_policy]
default_allow = false
rules = [
"identity.username == 'deployer'",
'''identity.oidc != null &&
identity.oidc.claims["ref"] == "refs/heads/main"'''
]

Verification

Test with curl

# Without auth
curl http://localhost:5000/v2/

# With basic auth
curl -u admin:password http://localhost:5000/v2/

# With bearer token
curl -H "Authorization: Bearer $TOKEN" http://localhost:5000/v2/

Debug Logging

RUST_LOG=angos::registry::access_policy=debug \
./angos server

Troubleshooting

All requests denied:

  • Check if any rule can match
  • For default_allow = false, at least one rule must return true
  • Enable debug logging to see rule evaluation

OIDC rules not matching:

  • Always check identity.oidc != null first
  • Use bracket notation: identity.oidc.claims["claim"]
  • Check claim values in debug logs

Rule evaluation errors:

  • Failed rules are skipped with a warning
  • Check for null access (e.g., identity.oidc.claims when not using OIDC)

Reference