Shared implementation contracts
Use the canonical Website reference for administrator settings, names, images, ports and status. Source types and the installed CRD define validation. Keep public guidance aligned with interface changes; historical plans are context, not a second specification.
1. Custom Resource
The authoritative schema is controller/config/crd/bases/cms.tlspu.com_websites.yaml. Update controller types/schema first, then mirror changes to deploy/01-crd.yaml and external Orbit copies. Preserve validation, defaults and upgrade compatibility described in Website settings.
Authentication Secret
The CMS reads one file per key under /etc/viking/auth, mounted read-only with
mode 0444. Keep operator validation and CMS parsing aligned with the canonical
Secret reference.
Invalid auth must block protected serving and readiness.
2. Resource naming
Preserve names, ownership, retained PVC identity and automatic identity records
across reconciliation. See generated names
and operator architecture. Status RBAC grants
get/patch/update on its own Website's status subresource by resource name.
3. Container images
See image configuration and build/test for production artifacts. Runtime components use static Go binaries, non-root scratch containers and read-only root filesystems. The CMS image carries the CA bundle for IdP/Kubernetes HTTPS and builds/tests its own editor. Node is a build dependency rather than a runtime.
4. Ports and endpoints
Canonical ports and probe behaviour
are customer-facing interfaces. Every HTTP listener must apply the component's
buildinfo.WithServerHeader outside authentication, using the binary's stamped
version, with successful and rejected response checks. Standard probes include
status/version JSON and Cache-Control: no-store; preserve status-code semantics.
5. Published filesystem layout
See the canonical published layout and publisher for immutable release, activation, history and recovery contracts.
6. CMS private layout
See the canonical private layout and persistence internals for source authority, durable mutations, recovery journals and snapshot lifetimes.
7. CMS API and authentication
JSON under /api/*; the SPA is served at /. The CMS backend is the
authentication boundary: it runs a built-in OAuth2/OIDC client, so every
request on the public :8080 listener is authenticated (the ingress performs
TLS offload only). Public auth endpoints and the authenticated API:
GET /oauth2/start— begin the Authorization Code login flow (public).GET /oauth2/callback— exchange the code, validate the ID token (issuer, audience, expiry, nonce) or fall back to userinfo, enforceallowedRoles, set the session cookie (public).POST /oauth2/logout— CSRF-protected logout: requires the CSRF session cookie + a matchingX-CSRF-Token+ a same-originOrigin/Referer; clears the session cookie and returns200 {"logoutURL": "<idp end-session URL>"}(or{"logoutURL": "/"}when the provider exposes no end-session endpoint). AGETrequest to/oauth2/logoutreturns405and performs no side effect.GET /api/me—{email, name, roles, issuer, subject}for the signed-in user;401when unauthenticated.GET/POST /api/pages,GET/PUT/DELETE /api/pages/{id},GET/PUT /api/settings,GET/POST/DELETE /api/assets,GET /api/assets/{id}/content,GET /api/publish/status,POST /api/publish,POST /api/publish/rollback— all behind the auth middleware.POST /api/content/render— render unsaved source without changing project state, also behind authentication. Page content fields and supported formats are defined in Content.PUT /api/pages/content?path=...&title=...&menu=false— upload raw UTF-8text/markdownortext/html(8 MiB maximum), replacing by normalized URI while retaining identity;201for creation,200for replacement.DELETE /api/pages/branch?path=...— journaled deletion of the root and all URI descendants;200withcountanddeletedpage summaries. Empty paths are rejected; explicit/deletes all draft pages. Neither operation publishes. Detailed query/menu semantics and OAuth examples are in CI content uploads.- Theme listing, authoring, sample/site preview and ZIP import/export endpoints are defined in Themes.
IdP endpoints are HTTPS-only: discoveryURL, authURL, tokenURL,
jwksURL, issuer, userinfoURL and introspectionURL must be https://.
Plain http:// is accepted only for loopback hosts (localhost, 127.0.0.1,
[::1]) and only with the CMS flag --allow-insecure-auth-http /
VIKING_ALLOW_INSECURE_AUTH_HTTP=true (development only); without it the CMS
refuses to start.
Unauthenticated behaviour: /api/* returns 401 (JSON); a browser-like
navigation to a non-/api path is redirected (302) to /oauth2/start; when
the auth configuration is missing or invalid the whole protected surface returns
503 and the CMS never runs unauthenticated.
Session cookie — viking_session (configurable via cookieName): a
normally stateless encrypted value
base64url(nonce‖AES-256-GCM-encrypted-claims).base64url(HMAC-SHA256(jwtSecret, blob))
containing email, name, roles, iss/sub, exp/iat, a random jti
and an ID-token hint retained for logout. The claims are AES-256-GCM encrypted
(key derived from jwtSecret) and the whole blob is HMAC-SHA256 signed, so the
cookie is opaque to anyone who can read it. It is HttpOnly, SameSite=Lax
and Secure (the operator sets VIKING_COOKIE_SECURE=true; the CMS defaults to
Secure).
Large session payloads use an encrypted reference cookie rather than exceeding browser limits. The full identity (including all roles) and logout token hint stay in a bounded in-memory cache (1,000 sessions, 16 MiB total, 256 KiB maximum per identity). These reference sessions require login again after a CMS restart or cache eviction. Small sessions remain self-contained; cookie name plus value is kept below 3,800 bytes.
Bearer tokens (machine clients) — an Authorization: Bearer header wins
over the session cookie. The token is validated as a JWT against the provider
JWKS (issuer always checked; audience = audience when set, else the client
id), or, when that fails and introspectionURL is set, via RFC 7662
introspection (client-credentials basic auth; the token must come back
active).
RBAC — when allowedRoles is set, the user must have at least one matching
role (from rolesClaim); otherwise any authenticated user passes. When
publishRoles is set, POST /api/publish and POST /api/publish/rollback
additionally require one of those roles (403 otherwise). RBAC applies to
browser sessions and Bearer tokens alike; denial returns 403.
CSRF — GET /api/publish/status is the CSRF bootstrap: it sets a per-session
cookie (viking_csrf, HttpOnly, SameSite=Strict, Secure when
--csrf-secure) and returns a random per-session token in the X-CSRF-Token
header and the csrfToken body field. Browser-session mutations must present the
session cookie, a matching X-CSRF-Token, and a same-origin Origin/Referer.
Verified Bearer identities are exempt from the cookie/token requirement on
/api/* mutations; an unverified Authorization header never grants exemption.
Same-origin checks apply to supplied Origin/Referer headers for all clients.
This is anti-CSRF only, never authorization — authorization is the auth
middleware described above. The short-lived login state store and CSRF sessions
are held in memory, so the CMS runs as a single replica. Supplied Origin or
Referer headers are checked; CLI requests without either header have no origin
context to reject. Browser clients should send the normal same-origin headers.
The admin listener (:8081) /healthz, /readyz and /metrics stay
unauthenticated so health checks and scraping do not need credentials.
8. Go modules
github.com/tlspu/viking-cms/controller
github.com/tlspu/viking-cms/frontend
github.com/tlspu/viking-cms/cms
A root go.work at the repository root declares use (./controller ./frontend ./cms) so the modules build together while remaining independent.
9. CMS authentication (built-in OAuth2/OIDC client)
The CMS is the authentication boundary: it runs the OAuth2/OIDC client itself,
so the ingress does not authenticate and carries no auth annotations (it
terminates TLS only). There is exactly one mechanism — a reference to the Secret
the CMS reads its client configuration from (cms.authentication.secretRef),
see the key table in section 1. The operator mounts the Secret read-only,
sets the auth env vars on the CMS Deployment, and validates the required keys;
it must reject configurations that would expose the CMS without effective
authentication (missing Secret, empty authentication, hostname without
authentication) rather than silently falling back to unauthenticated access.
The generated CMS Ingress is a plain routing Ingress.
Introspection responses must include active: true, iss matching the configured
provider issuer, and aud (a string or array) containing the configured
audience, or the client ID when no audience is configured. Present exp and
nbf claims are also enforced. Active responses with missing or mismatched
issuer/audience are rejected, including JWTs rejected by local verification.
Configure the provider to include these claims before upgrading clients that use
opaque tokens. Browser sessions and ordinary JWT verification are unaffected by
this introspection requirement.
Asset delivery and session recovery
Every newly published asset has an immutable api/assets/<id>/metadata.json
containing {"contentType":"<validated media type>"} beside its content file.
The frontend accepts only supported raster image, plain text and JSON types
from metadata and serves documents as attachments; raw uploads remain
application/octet-stream attachments with their stored filename. The /api/assets namespace
is reserved and cannot contain page drafts. Older releases without metadata
retain generic MIME delivery until republished.
GET /api/me also returns issuer and subject, identifying the authenticated
principal independently of optional profile fields. On expiry, the editor lets
the user continue local editing while server mutations remain blocked. Explicit
login uses the current window after checkpointing authoring drafts in per-tab
session storage. Recovery verifies the original principal and refreshes CSRF,
restores source and metadata, then clears the checkpoint. Reauthentication never
automatically saves or publishes. Credentials are excluded from checkpoints;
storage failure prevents navigation from silently discarding work.
