Skip to content

v0.15.0

Release Date: 2026-10-13

This release covers everything since v0.14.0. It has five themes: key management through keyforge, connections to external services through connectionforge, invite-only sign-up with a waitlist, an app catalog for product switchers, and the orgs claim with per-application roles in embedded SystemAuth. It has no breaking changes. Run systemauth migrate for the new tables.

Highlights

  • Key management. The keys package wires keyforge as the platform's one key library: envelope encryption, rotation without re-encrypting stored data, the root key read through omnivault, and key events sent to the audit trail. See Key management and ADR-013.
  • Connections to external services. The connections package composes connectionforge: an organization connects accounts at services such as a CDN or analytics product, credentials are sealed under managed keys, and an application receives an authenticated HTTP client, never the secret. See Connections and ADR-017.
  • A connections API. /api/v1/organizations/{org}/connections is served by SystemAuth when the new connections configuration section is set, behind the systemforge:connections:read and :write scopes.
  • Invite-only sign-up. Social login can admit only verified emails that hold an invite. Everyone else reaches a waitlist that records the consent text they were shown. systemauth invite and systemauth waitlist manage both. See Deploying SystemAuth.
  • App catalog. A host publishes the applications it runs at /.well-known/sf-apps, and /bff/session lists the catalog so each shell shows the products a person holds a role in for the current organization.
  • Per-application roles in tokens. Embedded SystemAuth emits the orgs claim with each organization's application roles, so relying parties keep no membership tables.
  • A PostgreSQL fix. rls.Helper.SetTenant and SetUser failed on every real PostgreSQL connection; they now work.

Upgrade notes

There are no breaking changes, but there are new tables and one new optional configuration section.

  1. Migrate. Run systemauth migrate (or systemauthsvc.Service.Migrate). It creates sf_signup_invites and sf_signup_waitlist. Existing tables are unchanged and there is no SQL migration file to apply by hand.
  2. Enabling connections is optional. Without the connections section nothing changes: no routes are mounted and no keys are opened.
  3. To enable them, add the section, create a root key, and back up the keys directory with the database:
connections:
  keys_dir: /var/lib/systemauth/keys
  vault: env://
  root_key: SYSTEMAUTH_KEYS_ROOT
export SYSTEMAUTH_KEYS_ROOT="$(openssl rand -base64 32)"   # keep a second copy offline

Connections need a database and the role-based authorization mode. systemauth migrate then creates the connectionforge_* tables and, on PostgreSQL, enables row-level security on them keyed on owner_id. Run the migration with a role allowed to change the schema. 4. Register providers. An embedding application passes the providers it supports through systemauthsvc.Options.Providers. Without any, the API serves an empty provider list. 5. Request the new scopes. Clients of the connections API ask for systemforge:connections:read and systemforge:connections:write.

Features

Key management

Piece Where it lives
Root key Your secret store, read through omnivault at startup
Key ring A file in the keys directory, sealed under the root key
Registry A SQLite database in the same directory
Data Your database, as ciphertext that names the key version
k, err := keys.Open(ctx, keys.Options{
    Dir:      "/var/lib/myapp/keys",
    Root:     keys.VaultSource(vault, "MYAPP_KEYS_ROOT"),
    Purposes: []keyforge.Purpose{"myapp.sessions"},
})
sealed, err := k.Seal(ctx, "myapp.sessions", []byte("value"), []byte("user:42"))

This release uses keyforge's single-host setup. Moving its registry to the platform database needs keyforge to accept an external registry and is recorded in ADR-013. keyforge is not externally audited.

Connections

  • Each connection is validated with its provider before it is stored, and a rejected credential stores nothing.
  • A connection holds no secret. Its credential is sealed under the connectionforge.credential purpose with additional data binding the organization, the connection and the credential version.
  • Organization owners and admins can view, use and manage connections; members can view them. A connection's creator manages it.
  • Requests made through a connection reach only its provider's hosts, over HTTPS, never a private address.
  • Tenant isolation holds twice: queries filter on the organization, and on PostgreSQL row-level security refuses another organization's rows.
Method and path Action
GET /api/v1/providers Provider manifests
GET /api/v1/organizations/{org}/connections Connections the caller may view
POST /api/v1/organizations/{org}/connections Connect and validate
GET, PATCH, DELETE .../connections/{id} Read, rename, disconnect
POST .../connections/{id}/test Validate again; the result is in health

Sign-up gating

  • SignupGate decides whether an upstream login may create a new account. It never affects people who already have one.
  • A refusal never says why, and goes to WithSignupDeniedURL (validated at startup) or to the waitlist page.
  • The waitlist records the consent text shown and its version, answers the same whether or not an address was already listed, and is rate-limited per client and per address.
  • serve --invite-only, systemauth invite add|list|revoke, and systemauth waitlist list [--csv] and remove.
  • A host binary embedding SystemAuth sets systemauthsvc.Options.InviteOnly, SignupDeniedURL and WaitlistPage, and mounts the same commands from signupgate/signupcmd in its own CLI.

App catalog and the orgs claim

  • appcatalog describes the applications a deployment hosts: identifier, name, launch URL, icon and order. Static serves one in the same process; Remote fetches what SystemAuth publishes, cached and kept through an outage.
  • systemauthsvc.Options.Apps publishes a catalog and warns about entries with no registered vocabulary, since nobody can hold a role in them.
  • systemauthsvc.Options.Vocabularies and DefaultEntitlements install the orgs claim source: application roles come from approle, limited to the default entitlements. An owner holds the top role of each entitled application, and a registered application outside the defaults stays closed.

Fixed

  • rls.Helper.SetTenant and SetUser on PostgreSQL. They issued SET LOCAL ... = $1, which PostgreSQL rejects because SET takes no bind parameters. They use set_config(name, value, true), which has the same transaction scope and takes the value as a parameter, so a value with a quote is safe. A test now runs it against a real server.

Dependencies

Module Version
github.com/plexusone/keyforge v0.1.0 (new)
github.com/plexusone/connectionforge v0.1.0 (new)
github.com/plexusone/omnivault v0.5.0 (new)
github.com/plexusone/omnistorage-core v0.5.2
github.com/go-chi/chi/v5 v5.4.0
github.com/grokify/guardsql v0.3.0
golang.org/x/crypto v0.58.0

github.com/yaronf/httpsign stays at v0.5.2: v0.6 removed NewJWSSignerV3, which aauth-go v0.2.0 still calls, and pulls in jwx/v4. Requires Go 1.26.

Known limitations

  • Connections support static API tokens only. GitHub App and OAuth connections follow in connectionforge.
  • Role-based authorization only for connections. Startup refuses connections with the relationship-based mode.
  • No grants API for connections yet. Only owners, admins and a connection's creator can use one; the resource-grant ladder is mapped but not exposed.
  • SystemAuth passes no audit store to keys, so key lifecycle events are not recorded there, and connection events go to the server log.
  • keyforge's control plane is not mounted, so there is no status or manual rotation endpoint yet.
  • keyforge and connectionforge are not externally audited.