A production-grade gRPC and REST API service for managing authorization and permissions in a microservices architecture. Built with Go 1.25, leveraging OpenFGA for fine-grained authorization, Valkey for caching, and PostgreSQL for persistent queue-based permission-update operations.
The Authorization Service (codename Authorization-service) is designed to provide:
- Permission Management: Register and manage service permissions via a versioned gRPC API
- Authorization Decisions: Perform authorization decisions using OpenFGA
- Envoy Integration: Implements Envoy's standard External Authorization API (v3) for use with Istio or other service meshes
- Caching: Uses Valkey for high-performance authorization decision caching
- Ingestion Pipeline: Highly scalable, async Kafka-based permission-update event ingestion
- Token Validation: Integrates with the Secure Token Service (STS) for token validation
graph TB
subgraph clients["Clients"]
A["Microservices"]
B["Envoy/Istio"]
C["External Apps"]
end
subgraph authz["Authorization Service"]
REST["REST API Gateway<br/>:8070"]
GRPC["gRPC Server<br/>:9091"]
PERMS["Permissions<br/>Service"]
AUTHZ["Authorization<br/>Service"]
REST -->|Transcode| GRPC
GRPC --> PERMS
GRPC -->|External AuthZ| AUTHZ
end
subgraph integrations["Integrations"]
FGA["OpenFGA<br/>Fine-Grained<br/>Authorization"]
VALKEY["Valkey<br/>Cache"]
STS["Secure Token<br/>Service"]
POSTGRES["Postgres DB<br/>Queue/State"]
KAFKA["Kafka Broker"]
end
subgraph external["External Services"]
ISTIO["Istio Service Mesh"]
end
A -->|Register Permissions| GRPC
B -->|External AuthZ Check| GRPC
C -->|REST Health Checks| REST
PERMS --> VALKEY
AUTHZ --> FGA
AUTHZ --> VALKEY
AUTHZ --> STS
ISTIO -->|Authorization Requests| GRPC
KAFKA -->|Permission Updates| GRPC
POSTGRES -->|Durability Queue| GRPC
- Cobra CLI: Professional command-line interface with automatic help generation and dedicated subcommands
- Versioned gRPC APIs with Go code generation from protobuf definitions
- REST API Gateway using grpc-gateway for transcoding gRPC to REST (currently exposes health endpoints)
- OpenFGA Integration: Fine-grained, relationship-based access control
- Dual Authentication: Envoy External Authorization supporting both user session cookies and machine-to-machine OAuth2 client credentials with strict mutual exclusivity and delegated token exchange
- Valkey Caching: Cache authorization decisions for ultra-low latency check responses
- Database Durability: PostgreSQL-backed transactional queue for robust, horizontally scalable ingestion
- Kafka-Based Ingestion: Listeners that consume permission changes asynchronously from federated services
- Graceful Shutdown: Strict connection cleanup and signal handling
- Go: 1.25 or later
- Docker: For running dependencies
- Docker Compose: For orchestrating containers
git clone https://github.com/canonical/authorization-service.git
cd authorization-servicego mod download
go mod tidymake buildThis creates bin/app binary.
# Start Docker dependencies (Postgres, Kafka, Valkey, OpenFGA, STS)
make start-deps
# Run the service (in another terminal)
./bin/app serveBuilds, starts dependencies, and runs with debug logging:
make devmake build # Build binary (bin/app)
make run # Run service
make start-deps # Start docker dependencies
make stop-deps # Stop docker dependencies
make test # Run unit tests
make test-int # Run integration tests
make coverage # Generate HTML coverage reportThe service uses Cobra to expose multiple operational subcommands:
./bin/app serveStarts both the gRPC server (default port 9091) and the REST gateway (default port 8070).
./bin/app migrate --dsn "postgres://authz:authz-password@localhost:5432/authorization-service?sslmode=disable" upRuns schema migrations against the PostgreSQL database. Supports up, down [version], and status actions.
./bin/app listenStarts the Kafka permission-update listener which consumes permission events from federated services' permissions.<slug> topics and persists them into the PostgreSQL work queue. Also exposes a health check HTTP endpoint (GET /healthz returning 200 OK with JSON body {"status":"SERVING"}) on the configured HTTP server address (<SERVER_HOST>:<HTTP_PORT>, default 0.0.0.0:8070).
By default, executing listen automatically runs topic creation (ensure-topics) before starting the listener. To disable automatic topic creation (for instance, if topics are pre-created or managed externally), pass the --no-topics flag:
./bin/app listen --no-topics./bin/app workerStarts the background worker that polls the PostgreSQL work queue and applies the pending permission changes to OpenFGA. Also exposes a health check HTTP endpoint (GET /healthz returning 200 OK with JSON body {"status":"SERVING"}) on the configured HTTP server address (<SERVER_HOST>:<HTTP_PORT>, default 0.0.0.0:8070).
./bin/app reaperScans the work queue for processing jobs that have timed out and returns them to the queue to be retried.
Provides subcommands for managing and inspecting Kafka permission-update topics:
-
topics ensure(alias:ensure-topics):./bin/app topics ensure
Creates the
permissions.<slug>Kafka topic for every federated service in the registry. Existing topics are left untouched, making this command safe to run repeatedly.Note: The
listencommand automatically executes topic creation on startup unless--no-topicsis specified. -
topics list(alias:list-topics):./bin/app topics list
Prints the resolved
permissions.<slug>Kafka topic names derived from the federated service registry (according to theFEDERATED_SERVICES_STRATEGY) to standard output.
Handles loading, verifying, and seeding versioned rules.yaml routing rules:
./bin/app seedSeeds embedded rules into the PostgreSQL database. Compares and replaces revisions atomically.
-
validateSubcommand: Validates route rules syntax and structure without requiring an active database or environment variables:# Validate default embedded rules ./bin/app seed validate # Validate rules under a custom directory ./bin/app seed validate --dir path/to/services
-
Dry-Run Mode: You can also perform the same config-independent validation using the dry-run flag:
./bin/app seed --dry-run
Provides utilities for checking permissions and writing models to OpenFGA directly:
-
checkSubcommand: Simulates an Envoy External AuthorizationCheckrequest using a session cookie to test STS integration:./bin/app authz check --cookie "session=abc123xyz" --path "/api/resource" --method "GET"
-
write-modelSubcommand: Compiles the modular authorization model from the embedded manifest and writes it to the specified OpenFGA store:./bin/app authz write-model <store-id>
./bin/app versionConfiguration is managed through Viper, providing a robust, multi-layered system. Configuration can be passed via:
- Cobra CLI Flags (highest priority)
- Environment Variables (nested paths mapped to structured uppercase words)
- YAML Config File (loaded from
./authorization-service.yaml,/etc/authz/authorization-service.yaml, or specified with-c/--config) - Go-defined Defaults (lowest priority)
To view or start with a baseline YAML configuration, refer to the provided authorization-service.yaml.example file.
The following flags are available globally across all CLI commands to quickly override settings:
-c, --config <path>: Path to a custom YAML configuration file.--grpc-port <int>: Overrides the gRPC server port.--http-port <int>: Overrides the HTTP port (used for the REST API Gateway onserveand the/healthzHTTP server onlistenandworker).--server-host <string>: Overrides the server bind host address.--db-host <string>: Overrides the PostgreSQL database host.--db-port <int>: Overrides the PostgreSQL database port.--db-name <string>: Overrides the PostgreSQL database name.--db-user <string>: Overrides the PostgreSQL database user.--dev: Enables development mode.--log-level <string>: Overrides the logging level.--log-format <string>: Overrides the logging format (jsonortext).--fga-address <string>: Overrides the OpenFGA server address.--fga-store-id <string>: Overrides the OpenFGA Store ID.--fga-model-id <string>: Overrides the OpenFGA Authorization Model ID.--fga-api-key <string>: Overrides the OpenFGA API key.
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
server.grpc_port |
SERVER_GRPC_PORT / GRPC_PORT |
int | 9091 |
gRPC server port |
server.http_port |
SERVER_HTTP_PORT / HTTP_PORT |
int | 8070 |
REST gateway port (serve) and health check HTTP port (listen, worker) |
server.host |
SERVER_HOST |
string | 0.0.0.0 |
Server bind address |
server.shutdown_timeout |
SERVER_SHUTDOWN_TIMEOUT |
duration | 15s |
Graceful shutdown timeout |
server.development |
SERVER_DEVELOPMENT |
bool | false |
Enable development mode (e.g. gRPC reflection) |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
ext_authz_service.jwk_set_url |
EXT_AUTHZ_SERVICE_JWK_SET_URL |
string | http://localhost:8080/.well-known/jwks.json |
JWKS URL to verify STS-issued internal JWT tokens |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
openfga.address |
OPENFGA_ADDRESS |
string | http://localhost:8081 |
OpenFGA server address |
openfga.store_id |
OPENFGA_STORE_ID |
string | (Required) | OpenFGA store ID |
openfga.authorization_model_id |
OPENFGA_AUTHORIZATION_MODEL_ID |
string | (Required) | OpenFGA authorization model ID |
openfga.api_key |
OPENFGA_API_KEY |
string | (Required) | OpenFGA authentication API token |
openfga.timeout |
OPENFGA_TIMEOUT |
duration | 10s |
Request timeout |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
valkey.enabled |
VALKEY_ENABLED |
bool | false |
Enable Valkey caching |
valkey.address |
VALKEY_ADDRESS |
string | localhost:6379 |
Valkey server address (Required if enabled) |
valkey.username |
VALKEY_USERNAME |
string | "" |
Valkey username |
valkey.password |
VALKEY_PASSWORD |
string | "" |
Valkey password |
valkey.db |
VALKEY_DB |
int | 0 |
Database index |
valkey.pool_size |
VALKEY_POOL_SIZE |
int | 10 |
Max connection pool size |
valkey.timeout |
VALKEY_TIMEOUT |
duration | 5s |
Read/write timeout |
valkey.use_tls |
VALKEY_USE_TLS |
bool | false |
Establish secure TLS connection |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
postgres.host |
POSTGRES_HOST |
string | localhost |
Database host |
postgres.port |
POSTGRES_PORT |
int | 5432 |
Database port |
postgres.user |
POSTGRES_USER |
string | authz |
Database user |
postgres.password |
POSTGRES_PASSWORD |
string | authz-password |
Database password |
postgres.db_name |
POSTGRES_DB_NAME |
string | authorization-service |
Database name |
postgres.ssl_mode |
POSTGRES_SSL_MODE |
string | disable |
SSL mode: disable, require, verify-ca, verify-full |
postgres.max_open_conns |
POSTGRES_MAX_OPEN_CONNS |
int | 25 |
Maximum open database connections |
postgres.max_idle_conns |
POSTGRES_MAX_IDLE_CONNS |
int | 5 |
Maximum idle database connections |
postgres.conn_max_lifetime |
POSTGRES_CONN_MAX_LIFETIME |
duration | 30m |
Maximum connection lifetime |
postgres.conn_max_idle_time |
POSTGRES_CONN_MAX_IDLE_TIME |
duration | 5m |
Maximum idle connection lifetime |
postgres.connect_timeout |
POSTGRES_CONNECT_TIMEOUT |
duration | 10s |
Connection timeout |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
sts.address |
STS_ADDRESS |
string | localhost:9090 |
Secure Token Service server address |
sts.use_tls |
STS_USE_TLS |
bool | false |
Establish secure TLS connection to STS |
sts.timeout |
STS_TIMEOUT |
duration | 10s |
Request timeout |
sts.eager_connection_check |
STS_EAGER_CONNECTION_CHECK |
bool | false |
Block startup until connection to STS is ready |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
kafka.enabled |
KAFKA_ENABLED |
bool | false |
Enable Kafka listener |
kafka.brokers |
KAFKA_BROKERS |
[]string | localhost:9092 |
Kafka broker addresses (Required if enabled) |
kafka.federated_services_strategy |
KAFKA_FEDERATED_SERVICES_STRATEGY |
string | auto |
Discovery strategy: auto (uses list if set, else scans authz/model), fs (scans embedded model folders excluding dummy/core), config (requires explicit list) |
kafka.federated_services |
KAFKA_FEDERATED_SERVICES |
[]string | [] |
Service slugs whose permission topics are to be federated (required if strategy is config) |
kafka.consumer_group |
KAFKA_CONSUMER_GROUP |
string | authz-listener |
Kafka consumer group ID |
kafka.topic_partitions |
KAFKA_TOPIC_PARTITIONS |
int | 1 |
Default partition count for auto-created topics |
kafka.topic_replication_factor |
KAFKA_TOPIC_REPLICATION_FACTOR |
int | 1 |
Default replication factor for auto-created topics |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
worker.enabled |
WORKER_ENABLED |
bool | false |
Enable the permission update background worker |
worker.batch_size |
WORKER_BATCH_SIZE |
int | 100 |
Processing batch size from work table |
worker.poll_interval |
WORKER_POLL_INTERVAL |
duration | 1s |
Ingestion poll interval |
worker.max_attempts |
WORKER_MAX_ATTEMPTS |
int | 5 |
Maximum retries before a row is marked failed |
worker.retry_backoff |
WORKER_RETRY_BACKOFF |
duration | 5m |
Delay before a failed row can be claimed again |
worker.stale_timeout |
WORKER_STALE_TIMEOUT |
duration | 15m |
Max duration a row can sit in processing state before reclamation |
worker.reaper_interval |
WORKER_REAPER_INTERVAL |
duration | 1m |
Execution interval of the in-process worker reaper |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
logging.level |
LOGGING_LEVEL |
string | info |
Log level: debug, info, warn, error |
logging.format |
LOGGING_FORMAT |
string | json |
Log output format: json or text |
| YAML Path | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
telemetry.enabled |
TELEMETRY_ENABLED |
bool | false |
Enable OTel tracer |
telemetry.otlp_endpoint |
TELEMETRY_OTLP_ENDPOINT |
string | "" |
OTLP gRPC endpoint |
telemetry.service_name |
TELEMETRY_SERVICE_NAME |
string | authorization-service |
Service name in exported traces |
telemetry.service_version |
TELEMETRY_SERVICE_VERSION |
string | v1.0.0 |
Service version in exported traces |
make testRequires running Docker environment:
make test-intGenerates code coverage maps:
make coverageView the results by opening coverage.html in your browser.
- Permissions Management Service:
authorization.service.api.v1.PermissionsServiceRegisterPermissionsGetPermissionsListPermissionsDeletePermissions
- Envoy External Authorization:
envoy.service.auth.v3.AuthorizationCheck
GET /healthz- Liveness/readiness health check exposed acrossserve,listen, andworkerviainternal/health(returning200 OKwithContent-Type: application/jsonand body{"status":"SERVING"}). Note: The/v1/permissionsroutes are defined in proto definitions but are currently comment-disabled ingateway.goand require direct gRPC client calls.
The Authorization Service implements Envoy's External Authorization API (envoy.service.auth.v3.Authorization/Check), providing policy-based access control and token minting for microservice meshes (e.g., Istio).
The service supports two distinct credential mechanisms:
-
User Session Authentication:
- Credential:
Cookie: session_id=<cookie_value>. - Flow: The session cookie is exchanged with the Secure Token Service (STS) via
ExchangeSession. The returned STS JWT is verified againstext_authz_service.jwk_set_url. - Identity Mapping: The user subject (
sub) is extracted from the internal STS JWT and mapped to OpenFGA tuple subjectuser:<user_id>. - Downstream Header: On successful evaluation, Envoy's
OkHttpResponseinjectsAuthorization: Bearer <sts_jwt>.
- Credential:
-
Machine-to-Machine (M2M) Authentication:
- Credential:
Authorization: Bearer <token>(case-insensitive header extraction). - Flow: The raw bearer token is forwarded directly to the Secure Token Service (STS) via
ExchangeTokenwithout edge signature or issuer verification in Authorization Service. STS validates the token against its upstream identity provider (e.g., Ory Hydra) and returns an internal STS JWT. - Internal Verification: The returned STS JWT is cryptographically verified against the local STS JWKS endpoint (
ext_authz_service.jwk_set_url). - Zero-Bypass Policy Evaluation: The machine client ID (
sub) is extracted from the internal STS JWT and evaluated directly in OpenFGA asuser:<client_id>. Machine identities share the exact same relationship model and policy checks as human users without any bypass or shortcut. - Downstream Header: Envoy's
OkHttpResponseinjects the minted internal STS token asAuthorization: Bearer <sts_jwt>, giving downstream services a uniform token format regardless of caller type.
- Credential:
To prevent credential confusion, ambiguous caller identity, or security bypass vulnerabilities, External AuthZ enforces strict mutual exclusivity between credentials:
- Conflicting Credentials: If a request supplies both a session cookie (
Cookie: session_id=...) and a Bearer token (Authorization: Bearer <token>), the service immediately denies the request with HTTP 400 Bad Request (codes.InvalidArgument, response body"conflicting credentials provided"). The decision is recorded with metric outcomeconflicting_credentialsandauth_type: "none". - Missing Credentials: If a request supplies neither a session cookie nor a Bearer token, the request is denied with HTTP 401 Unauthorized (
codes.Unauthenticated, response body"no credentials provided"). The decision is recorded with metric outcomeno_credentialsandauth_type: "none".
Service onboarding and federation into Authorization-service are managed through a dedicated pull request workflow, treating the PR as the primary source of truth for configuration and metadata.
If you are a team onboarding your service, you can open your pull request with the dedicated federation template pre-loaded by clicking the button below:
Alternatively, you can manually select the federation_onboarding.md template when opening your Pull Request.
See CONTRIBUTING.md for guidelines.
See LICENSE file for details.
