Skip to content

navicore/anz

Repository files navigation

anz

A minimal, personal OIDC provider written in Rust. Secures personal web applications with standard OpenID Connect authentication. Designed for a single operator managing a small set of users and applications.

Why

Keycloak is too much. Auth0 costs money. This is a single binary with a SQLite database that implements the OIDC spec well enough to put behind a reverse proxy and protect a handful of personal apps.

Features

  • Multi-realm — isolated identity domains (users, clients, tokens)
  • OIDC authorization code flow with PKCE
  • Multi-algorithm signing — RS256 (default, required by Kubernetes) or EdDSA, per-realm, with concurrent keys during rotation
  • Argon2id password hashing
  • TOTP multi-factor auth — per-realm enforced or per-user opt-in; recovery codes; in-line enrollment during login
  • Refresh token rotation with RFC 7009 revocation
  • Confidential clients — optional client_secret for server-side apps (Forgejo, etc.)
  • Groups claimgroups array in ID tokens and UserInfo for RBAC (Kubernetes, etc.)
  • Rate limiting — per-IP login + per-user MFA attempt throttling
  • Audit logging — JSON-line event log for login, token, and session activity
  • Per-realm branding — customizable login page (colors, logo, CSS)
  • Minimal login UI — server-rendered HTML, no JavaScript frameworks
  • CLI admin — no admin web UI, just anz realm/user/client/session commands
  • SQLite — single file, embedded, no external database

Quick Start

cp anz.toml.example anz.toml
# edit anz.toml with your issuer URL

anz realm create myapp
anz user add --realm myapp --username alice --email alice@example.com
anz client add --realm myapp --client-id myapp-web --redirect-uri http://localhost:3000/callback
anz serve

Configuration

See anz.toml.example:

bind_address = "127.0.0.1:8080"
issuer_base_url = "https://auth.example.com"
database_path = "anz.db"

Deploy behind a TLS-terminating reverse proxy (nginx, caddy, etc.). Set issuer_base_url to your public HTTPS URL — Kubernetes and other OIDC consumers require HTTPS and will reject tokens from HTTP issuers.

Confidential clients authenticate via client_secret_post (secret in the POST body) or client_secret_basic (HTTP Basic auth header).

OIDC Endpoints

All endpoints are realm-scoped:

Endpoint Path
Discovery GET /realms/{realm}/.well-known/openid-configuration
JWKS GET /realms/{realm}/jwks
Authorize GET /realms/{realm}/authorize
Token POST /realms/{realm}/token
UserInfo GET /realms/{realm}/userinfo
Password POST /realms/{realm}/password
Revoke POST /realms/{realm}/revoke

CLI

anz realm create <name> [--key-type rs256|ed25519]   # defaults to rs256
anz realm list
anz realm delete <name>
anz realm rotate-key --realm <r> --key-type rs256|ed25519
anz realm deactivate-key --realm <r> --kid <kid>  # stops signing; kid still in JWKS
anz realm delete-key --realm <r> --kid <kid>      # removes kid from JWKS entirely
anz realm set-mfa-required --realm <r>            # force MFA for all users
anz realm set-mfa-required --realm <r> --required false
anz user add --realm <r> --username <u> --email <e> [--groups admin,dev]
anz user update-groups --realm <r> --username <u> --groups <g1,g2>
anz user enroll-mfa --realm <r> --username <u>    # prints QR + recovery codes
anz user disable-mfa --realm <r> --username <u>   # operator escape hatch
anz user list --realm <r>
anz user remove --realm <r> --username <u>
anz client add --realm <r> --client-id <id> --redirect-uri <uri> [--secret]
anz client list --realm <r>
anz client remove --realm <r> --client-id <id>
anz session list --realm <r> --username <u>
anz session revoke --realm <r> --username <u>
anz session cleanup
anz serve

Docker

docker pull ghcr.io/navicore/anz:latest
docker run -v ./anz.toml:/etc/anz/anz.toml -v ./data:/data -p 8080:8080 \
  ghcr.io/navicore/anz --config /etc/anz/anz.toml serve

Releasing

Create a GitHub release with a tag like v0.2.0. The workflow automatically:

  1. Runs CI checks
  2. Bumps Cargo.toml version to match the tag and commits to main
  3. Generates a CycloneDX SBOM from Cargo.lock
  4. Builds and pushes a Docker image to GHCR with SBOM and SLSA provenance attestations
  5. Signs the image and attaches the Cargo SBOM via Cosign (keyless, GitHub OIDC)

Required repo secret: PAT (GitHub token with contents: write).

Verify a release (replace tag with actual version, or use @sha256:... digest for strongest guarantee):

cosign verify \
  --certificate-identity-regexp "https://github.com/navicore/anz/.github/workflows/release.yml@refs/tags/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/navicore/anz:0.3.0

cosign verify-attestation --type cyclonedx \
  --certificate-identity-regexp "https://github.com/navicore/anz/.github/workflows/release.yml@refs/tags/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/navicore/anz:0.3.0

The identity uses refs/tags/.* because releases are triggered by tag creation — the OIDC token GitHub mints for the workflow records the tag ref, not main.

All GitHub Actions are pinned to commit SHA (not version tags) to prevent supply chain attacks via tag mutation.

Development

# run the same checks as CI (format, clippy, tests, release build)
just ci

# format + build + test
just dev

CI runs just ci — the justfile is the single source of truth. Linux on PRs, macOS on merge to main.

Requires just and a Rust toolchain.

About

authn authz in rust

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors