Auth Server
GoThe VM Registry Auth Server (vm-registry-auth) is a standalone authentication and authorization service for the VM Registry ecosystem. It manages user identities, issues JWT tokens for registry access, and enforces repository-level access control lists (ACLs). The auth server is backed by PostgreSQL and uses RSA key pairs for cryptographic token signing.
Overview
The auth server sits alongside the registry server and provides the identity layer for the entire system. When users run vmr login, the request is proxied through the daemon to this server. On successful authentication, the server issues a JWT access token that the CLI stores locally. Subsequent vmr push and vmr pull operations include this token, which the registry server validates using the shared RSA public key.
CLI ──► Daemon ──HTTP──► Auth Server ──► PostgreSQL
│
└── RSA private key (signs tokens)
Registry Server ──► RSA public key (verifies tokens)Running the Auth Server
Prerequisites
- PostgreSQL 17+ (or any recent version)
- RSA key pair for JWT signing
1. Generate RSA Keys
mkdir -p keys
openssl genrsa -out keys/auth.key 4096
openssl rsa -in keys/auth.key -pubout -out keys/auth.pubThe private key (auth.key) is used by the auth server to sign tokens. The public key (auth.pub) is distributed to the registry server for token verification.
2. Start PostgreSQL
Use the provided docker-compose.yml:
cd vm-registry-auth
docker compose up -d postgresThis starts PostgreSQL on 127.0.0.1:5432 with the following defaults:
| Parameter | Value |
|---|---|
| User | admin |
| Password | admin |
| Database | registry_auth_db |
| Port | 5432 |
The init.sql schema is automatically applied via Docker's entrypoint mechanism.
3. Start the Auth Server
DATABASE_URL="postgresql://admin:admin@localhost:5432/registry_auth_db" go run server/main.goThe auth server listens on port 4078.
Using Docker Compose (Full Stack)
The docker-compose.yml can run both the auth server and PostgreSQL together:
docker compose up -dConfiguration
The auth server is configured through environment variables:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | yes | PostgreSQL connection string (e.g., postgresql://user:pass@host:5432/dbname) |
The RSA private key is read from ./keys/auth.key relative to the server's working directory. The key file must be in PEM format.
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /register | Register a new user account |
POST | /login | Authenticate and receive access + refresh tokens |
GET | /token | Registry token endpoint — issue scoped access tokens |
POST | /logout | Revoke refresh tokens and log out |
GET | /health | Health check |
POST /register
Creates a new user account. Passwords are hashed with bcrypt before storage.
Request body:
| Field | Type | Description |
|---|---|---|
username | string | Desired username |
password | string | Desired password |
On successful registration, default ACL rules are created for the user granting full access to their own namespace (<username>/*).
POST /login
Authenticates a user and returns JWT tokens.
Request body:
| Field | Type | Description |
|---|---|---|
username | string | Username |
password | string | Password |
Response:
| Field | Type | Description |
|---|---|---|
status | string | Result status |
message | string | Human-readable message |
access_token | string | JWT access token for API authentication |
GET /token
The registry token endpoint, following a pattern inspired by the Docker token authentication specification. The registry server redirects unauthenticated requests here to obtain scoped tokens.
POST /logout
Revokes the user's refresh tokens, effectively ending the session. The daemon calls this endpoint when the user runs vmr logout.
GET /health
Returns a simple health status. Used by vmr ping to verify the auth server is reachable.
Database Schema
The auth server uses three PostgreSQL tables:
users
| Column | Type | Description |
|---|---|---|
id | SERIAL PRIMARY KEY | Auto-incrementing user ID |
username | VARCHAR(255) UNIQUE NOT NULL | Unique username |
password_hash | VARCHAR(255) NOT NULL | Bcrypt-hashed password |
created_at | TIMESTAMPTZ NOT NULL | Account creation timestamp |
acl_rules
| Column | Type | Description |
|---|---|---|
id | SERIAL PRIMARY KEY | Auto-incrementing rule ID |
user_id | INTEGER NOT NULL | Foreign key to users.id (CASCADE delete) |
repository_pattern | VARCHAR(255) NOT NULL | Repository pattern (e.g., myapp, namespace/*, *) |
actions | TEXT[] NOT NULL | Allowed actions array (e.g., ['pull', 'push'] or ['*']) |
created_at | TIMESTAMPTZ NOT NULL | Rule creation timestamp |
refresh_tokens
| Column | Type | Description |
|---|---|---|
id | SERIAL PRIMARY KEY | Auto-incrementing token ID |
user_id | INTEGER NOT NULL | Foreign key to users.id (CASCADE delete) |
token | VARCHAR(255) UNIQUE NOT NULL | Refresh token value |
expires_at | TIMESTAMPTZ NOT NULL | Token expiration timestamp |
created_at | TIMESTAMPTZ NOT NULL | Token creation timestamp |
is_revoked | BOOLEAN NOT NULL DEFAULT FALSE | Whether the token has been revoked |
Indexes are created on users.username, acl_rules.user_id, refresh_tokens.token, and refresh_tokens.user_id for efficient lookups.
Access Control
ACL rules use repository pattern matching to determine what actions a user can perform:
| Pattern | Matches |
|---|---|
myapp | Exactly the myapp repository |
namespace/* | Any repository under the namespace/ prefix |
* | All repositories (superuser access) |
| Action | Description |
|---|---|
pull | Download images from matching repositories |
push | Upload images to matching repositories |
* | All actions on matching repositories |
When a user registers, the auth server automatically creates a default ACL rule granting * (all actions) on <username>/*, giving the user full control over their own namespace.
JWT Token Structure
Access tokens are RSA-signed JWTs containing:
| Claim | Description |
|---|---|
sub | Username |
iss | Token issuer identifier |
iat | Issued-at timestamp |
exp | Expiration timestamp |
access | Array of scope objects describing allowed repository actions |
The registry server validates tokens by:
- Verifying the RSA signature using the shared public key
- Checking the
expclaim to ensure the token has not expired - Parsing the
accessclaim to determine the granted scopes - Matching the requested operation against the granted scopes
Token Lifecycle
The auth server manages token lifecycle with automatic background cleanup:
- Access tokens are short-lived JWTs — the registry server rejects expired tokens at validation time
- Refresh tokens are stored in PostgreSQL with an explicit expiration timestamp
- A background task runs every hour to clean up:
- Expired refresh tokens (past their
expires_attimestamp) - Revoked refresh tokens older than 30 days
- Expired refresh tokens (past their
When a user logs out, all associated refresh tokens are marked as revoked immediately.
Internal Structure
| Package | Responsibility |
|---|---|
internal/api | HTTP router, route handlers (register, login, logout, token, health) |
internal/auth | Authentication logic (password verification) and authorization logic (scope validation, token generation) |
internal/database | PostgreSQL operations — user CRUD, ACL queries, token management, cleanup |
internal/logs | Structured logging with HTTP middleware |
internal/models | Data models for users, ACL rules, and tokens |
Dependencies
| Dependency | Purpose |
|---|---|
github.com/golang-jwt/jwt/v5 | JWT token creation and signing |
github.com/jackc/pgx/v5 | PostgreSQL driver and connection pooling |
golang.org/x/crypto | Bcrypt password hashing |
github.com/mattn/go-isatty | Terminal detection for log formatting |