Releases: SUNET/inmor
Release 0.3.0
0.3.0 — 2026-02-27
Added
-
POST /subordinates/fetch-config API endpoint to fetch and
self-validate OpenID Federation entity configurations -
Handle network errors gracefully (DNS failures, timeouts, 404s,
invalid JWTs) with user-friendly error messages -
Frontend for the Admins
-
JSON editor for all JSON fields
-
API_KEYS for API access #122.
-
MFA support #123
-
Management commands for Trust Mark Types and Subordinates
-
Enforce
kidheader in Trust Anchor JWT verification #150 -
Migrate Subordinate metadata/jwks fields from CharField to JSONField #158
-
Refetch metadata button to the frontend #164
-
renewal API and frontend update #166
-
Adds /collection endpoint back #170
-
Management command for subordinate renewal #174
-
apikey management command #178
-
production docker compose #137
-
granian to run admin django application #61
Fixed
-
Explicitly reject
alg: nonein JWT verification #151 -
/trust_mark_list returns JSON error responses instead of plain text #148
-
/fetch returns 400 instead of 500 on invalid entity configuration #147
-
/list with trust_marked=false no longer incorrectly filters subordinates #146
-
Authenticated API calls pass credentials correctly #152
-
Minimal
expclaim handling in trust mark JWTs #145 -
don't include null metadata in policy document
-
pass correct policy object to apply_policy and upgrade oidfpolicy to 0.2.0 #185
-
/listendpoint fix #187
Upgrade Plan: Inmor 0.2.0 → 0.3.0 (Production)
This document covers upgrading a production Inmor deployment from 0.2.0 to 0.3.0.
Frontend is not covered — this plan assumes you are not running the frontend service.
1. Backup
1.1 PostgreSQL
docker compose exec db pg_dump -U postgres postgres > backup_0.2.0.sql1.2 Redis
docker compose exec redis redis-cli save
docker cp <redis_container>:/data/dump.rdb ./backup_0.2.0.rdb1.3 Configuration files
cp docker-compose.yml docker-compose.yml.bak
cp taconfig.toml taconfig.toml.bak1.4 Verify backups
# Check SQL dump is non-empty
ls -lh backup_0.2.0.sql
# Check Redis dump
ls -lh backup_0.2.0.rdb2. Update taconfig.toml
Two changes from 0.2.0:
# Was: domain = "http://localhost:8080"
domain = "https://your-domain:8080"
redis_uri = "redis://redis:6379"
tls_cert = "tls.pem" # must match the container path in docker-compose.yml
tls_key = "tls-key.pem" # must match the container path in docker-compose.yml
# New in 0.3.0 — set to false for production (default)
allow_http = false- The
domainscheme changed fromhttptohttps. allow_http = falseis new and enables SSRF protection (blocks private IPs and
non-HTTPS federation requests).
3. Generate secrets
# MFA encryption key (required even if you don't use MFA yet — the app expects it)
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# Django SECRET_KEY (if you don't already have one in localsettings.py)
python3 -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# Pick a PostgreSQL password4. Create or update localsettings.py
This file is now mounted into the container (no longer optional). Create it at the
repo root:
SECRET_KEY = "your-generated-secret-key"
DEBUG = False
ALLOWED_HOSTS = ["your-domain.example.com", "admin"]
# HTTPS security
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Reverse proxy settings — REQUIRED when running behind nginx/Caddy/Apache.
# The proxy terminates TLS and forwards plain HTTP to Django. Without these
# settings Django sees http:// requests and issues 301 redirects, which
# breaks POST requests.
SECURE_SSL_REDIRECT = False # proxy handles HTTPS redirect
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # trust proxy header
# CORS — only needed if something external hits the admin API
CORS_ALLOWED_ORIGINS = ["https://your-domain.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://your-domain.example.com"]
# MFA encryption key
MFA_ENCRYPTION_KEY = "your-generated-fernet-key"
# If you switched to password auth for PostgreSQL:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "postgres",
"USER": "postgres",
"PASSWORD": "your-postgres-password",
"HOST": "db",
"PORT": 5432,
}
}5. Replace docker-compose.yml
The new production compose file has significant changes from 0.2.0:
| Area | 0.2.0 | 0.3.0 |
|---|---|---|
| TA image | inmor:0.2.2 |
inmor:0.3.0 |
| Admin image | inmor-admin:0.2.2 |
inmor-admin:0.3.0 |
| TA binary | debug build | release build |
| Admin WSGI | Django dev server | granian (production) |
| TA healthcheck | CMD true (no-op) |
Actual curl to /health |
| DB volumes | bind mount ./db |
named volume postgres_data |
| Redis volumes | bind mount ./redis |
named volume redis_data |
| DB ports | 5432:5432 exposed |
not exposed |
| Admin ports | 8000:8000 exposed |
not exposed (reverse proxy) |
| DB auth | POSTGRES_HOST_AUTH_METHOD=trust |
POSTGRES_PASSWORD (recommended) |
| TLS mounts | ./localhost+2.pem |
/etc/ssl/certs/inmor.pem (your cert path) |
| Volume flags | none | :ro on all config/key mounts |
| Admin volumes | source bind mounts | baked into image; only localsettings.py, private.json, publickeys/, historical_keys/ mounted |
| New env vars | — | PRODUCTION=true, RUST_LOG=info |
| Templates | not mounted | mounted as :ro on TA |
After copying the new docker-compose.yml from the repo, edit it:
- Set your TLS cert paths (the TA service volume mounts).
- Set
POSTGRES_PASSWORD. - Uncomment
MFA_ENCRYPTION_KEYand set it.
If you are currently using bind-mount ./db and ./redis: The new compose uses
named volumes. You can either migrate the data to named volumes, or keep using bind
mounts by changing postgres_data:/var/lib/postgresql/data back to
./db:/var/lib/postgresql/data (and similarly for Redis).
6. Database migration plan
The entrypoint runs these automatically on startup, but here is what happens:
Step 1 — pre_migrate_check (new command): Converts any empty strings in the
metadata and forced_metadata CharField columns to valid JSON ({}) before the
schema migration runs. This prevents the CharField → JSONField migration from
failing.
Step 2 — migrate applies these new migrations:
| Migration | What it does |
|---|---|
entities.0003_subordinate_metadata_jsonfields |
Converts metadata and forced_metadata from CharField to JSONField |
apikeys.0001_initial |
Creates APIKey table |
apikeys.0002_add_tenant_field |
Adds tenant column to APIKey |
auditlog.0001_initial |
Creates AuditLogEntry table with 4 indexes |
| django-allauth migrations | Creates allauth account_* and mfa_* tables |
django.contrib.sites |
Creates django_site table (required by allauth, SITE_ID = 1) |
Step 3 — collectstatic (new): Collects static files for whitenoise serving.
Step 4 — reload_issued_tms (unchanged): Reloads trust marks into Redis.
7. New Django apps and middleware
INSTALLED_APPS gained:
apikeys.apps.ApikeysConfigauditlog.apps.AuditlogConfigdjango.contrib.sitesdjango.contrib.humanizecorsheadersallauth,allauth.account,allauth.mfa
New middleware (order matters):
whitenoise.middleware.WhiteNoiseMiddleware— afterSecurityMiddlewarecorsheaders.middleware.CorsMiddleware— afterWhiteNoiseMiddlewareallauth.account.middleware.AccountMiddleware— afterAuthenticationMiddleware
URL config adds: path("accounts/", include("allauth.urls"))
These are all baked into the Docker image — no action required unless you have a
custom localsettings.py that overrides INSTALLED_APPS or MIDDLEWARE.
8. New Python dependencies
Added to the admin image:
| Package | Purpose |
|---|---|
django-allauth |
Authentication framework + MFA |
django-cors-headers |
CORS headers for API |
fido2 |
WebAuthn support |
whitenoise |
Static file serving in production |
qrcode |
QR codes for TOTP setup |
cryptography |
MFA secret encryption |
granian |
Production WSGI server (replaces Django runserver) |
All baked into the Docker image — no manual installation needed.
9. Rust/TA changes
Security hardening (built into the new binary)
- SSRF protection: Federation outbound requests block private IPs and require HTTPS
(controlled byallow_httpintaconfig.toml). - HTTP timeouts: 5s connect, 10s total, 2MB response limit, 5 redirect hops max.
- Recursion depth limits: Tree walking capped to prevent DoS.
- Redis key sanitization: Validates all user input used in Redis key construction.
kidheader enforcement: Rejects JWTs withoutkidor withalg: none.- JSON error responses:
/trust_mark_listand/fetchnow return JSON errors
instead of plain text or 500s.
New features
/healthendpoint: Verifies Redis connectivity (used by Docker healthcheck)./statusendpoint: Detailed operational status (keys, subordinates, trust marks,
collection counts)./collectionendpoint: Entity collection data (populated byinmor-collection
CLI).inmor-collectionbinary: New CLI that walks federation trees and populates
Redis. Run it periodically if you use the/collectionend...
Release 0.2.1
Release 0.2.0
Inmor 0.2.0 Release Notes
Release Date: 2026-01-13
Breaking Changes
Public Keys Directory (CRITICAL)
The single public.json file has been replaced with a publickeys/ directory that can contain multiple public keys. This affects both the TA and Admin services.
Migration Steps:
- Create a
publickeys/directory in your deployment folder - Move your existing
public.jsonintopublickeys/(keep the same filename) - Create an empty
historical_keys/directory for future key rotation support
mkdir -p publickeys historical_keys
mv public.json publickeys/Settings Changes (Admin)
If you have a custom localsettings.py:
SIGNING_PUBLIC_KEYis nowSIGNING_PUBLIC_KEYS(a list, auto-populated frompublickeys/directory)- Remove
metadatafromPOLICY_DOCUMENTif present - forced metadata is now per-subordinate in the database
Docker Compose Updates
If you wrote your own docker-compose.yml based on 0.1.3, make these changes:
1. Update Image Tags
# Change from:
image: docker.sunet.se/inmor:0.1.3
image: docker.sunet.se/inmor-admin:0.1.3
# To:
image: docker.sunet.se/inmor:0.2.0
image: docker.sunet.se/inmor-admin:0.2.02. Update TA Service Volumes
# REMOVE this line:
- ./public.json:/app/public.json
# ADD these lines:
- ./publickeys:/app/publickeys
- ./historical_keys:/app/historical_keys
# OPTIONAL - for TLS support:
- ./your-cert.pem:/app/your-cert.pem
- ./your-key.pem:/app/your-key.pem3. Update Admin Service Volumes
# REMOVE this line:
- ./public.json:/app/public.json
# ADD these lines:
- ./publickeys:/app/publickeys
- ./historical_keys:/app/historical_keysComplete Volume Diff
TA service volumes (before → after):
volumes:
- ./taconfig.toml:/app/taconfig.toml
- ./private.json:/app/private.json
- - ./public.json:/app/public.json
+ - ./publickeys:/app/publickeys
+ - ./historical_keys:/app/historical_keys
+ # Optional TLS certificates:
+ - ./localhost+2.pem:/app/localhost+2.pem
+ - ./localhost+2-key.pem:/app/localhost+2-key.pemAdmin service volumes (before → after):
volumes:
- ./admin/private.json:/app/private.json
- - ./public.json:/app/public.json
+ - ./publickeys:/app/publickeys
+ - ./historical_keys:/app/historical_keysNew Features
- Historical Keys Endpoint - New
/historical_keysendpoint for key rotation. Useadd_historical_key.pyscript to mark keys as expired/revoked. - TLS Support - TA can now serve over TLS. Add
tls_certandtls_keytotaconfig.toml:tls_cert = "your-cert.pem" tls_key = "your-key.pem"
- TrustMarks for TA - The Trust Anchor itself can now have TrustMarks from external issuers
- Multiple Key Types - Both TA and Admin now support different key algorithms (not just ES256)
entity_typeParameter - New parameter in/resolveendpointtrust_mark_typein/list- Filter subordinates by trust mark type- Forced Metadata - Apply forced metadata per-subordinate in entity statements
Fixes
/trust_mark_statusnow correctly accepts POST requests per OpenID Federation spec/resolveoutput contains fixed metadata, with applied forced metadata- TA now verifies it is listed in
authority_hintsbefore issuing subordinate statements - Improved error handling throughout the Rust codebase