Skip to content

Auth Server

Go

The 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.

text
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

bash
mkdir -p keys
openssl genrsa -out keys/auth.key 4096
openssl rsa -in keys/auth.key -pubout -out keys/auth.pub

The 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:

bash
cd vm-registry-auth
docker compose up -d postgres

This starts PostgreSQL on 127.0.0.1:5432 with the following defaults:

ParameterValue
Useradmin
Passwordadmin
Databaseregistry_auth_db
Port5432

The init.sql schema is automatically applied via Docker's entrypoint mechanism.

3. Start the Auth Server

bash
DATABASE_URL="postgresql://admin:admin@localhost:5432/registry_auth_db" go run server/main.go

The auth server listens on port 4078.

Using Docker Compose (Full Stack)

The docker-compose.yml can run both the auth server and PostgreSQL together:

bash
docker compose up -d

Configuration

The auth server is configured through environment variables:

VariableRequiredDescription
DATABASE_URLyesPostgreSQL 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

MethodPathDescription
POST/registerRegister a new user account
POST/loginAuthenticate and receive access + refresh tokens
GET/tokenRegistry token endpoint — issue scoped access tokens
POST/logoutRevoke refresh tokens and log out
GET/healthHealth check

POST /register

Creates a new user account. Passwords are hashed with bcrypt before storage.

Request body:

FieldTypeDescription
usernamestringDesired username
passwordstringDesired 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:

FieldTypeDescription
usernamestringUsername
passwordstringPassword

Response:

FieldTypeDescription
statusstringResult status
messagestringHuman-readable message
access_tokenstringJWT 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

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing user ID
usernameVARCHAR(255) UNIQUE NOT NULLUnique username
password_hashVARCHAR(255) NOT NULLBcrypt-hashed password
created_atTIMESTAMPTZ NOT NULLAccount creation timestamp

acl_rules

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing rule ID
user_idINTEGER NOT NULLForeign key to users.id (CASCADE delete)
repository_patternVARCHAR(255) NOT NULLRepository pattern (e.g., myapp, namespace/*, *)
actionsTEXT[] NOT NULLAllowed actions array (e.g., ['pull', 'push'] or ['*'])
created_atTIMESTAMPTZ NOT NULLRule creation timestamp

refresh_tokens

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing token ID
user_idINTEGER NOT NULLForeign key to users.id (CASCADE delete)
tokenVARCHAR(255) UNIQUE NOT NULLRefresh token value
expires_atTIMESTAMPTZ NOT NULLToken expiration timestamp
created_atTIMESTAMPTZ NOT NULLToken creation timestamp
is_revokedBOOLEAN NOT NULL DEFAULT FALSEWhether 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:

PatternMatches
myappExactly the myapp repository
namespace/*Any repository under the namespace/ prefix
*All repositories (superuser access)
ActionDescription
pullDownload images from matching repositories
pushUpload 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:

ClaimDescription
subUsername
issToken issuer identifier
iatIssued-at timestamp
expExpiration timestamp
accessArray of scope objects describing allowed repository actions

The registry server validates tokens by:

  1. Verifying the RSA signature using the shared public key
  2. Checking the exp claim to ensure the token has not expired
  3. Parsing the access claim to determine the granted scopes
  4. 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_at timestamp)
    • Revoked refresh tokens older than 30 days

When a user logs out, all associated refresh tokens are marked as revoked immediately.

Internal Structure

PackageResponsibility
internal/apiHTTP router, route handlers (register, login, logout, token, health)
internal/authAuthentication logic (password verification) and authorization logic (scope validation, token generation)
internal/databasePostgreSQL operations — user CRUD, ACL queries, token management, cleanup
internal/logsStructured logging with HTTP middleware
internal/modelsData models for users, ACL rules, and tokens

Dependencies

DependencyPurpose
github.com/golang-jwt/jwt/v5JWT token creation and signing
github.com/jackc/pgx/v5PostgreSQL driver and connection pooling
golang.org/x/cryptoBcrypt password hashing
github.com/mattn/go-isattyTerminal detection for log formatting

Built with Go and Rust