Skip to main content

SECURITY_MODEL — theme-config-service

Sibling: API_CONTRACTS · DATA_MODEL · AI_INTEGRATION

Platform anchors: docs/07-security-compliance-tenancy.md · docs/architecture/ADR-0002-multi-tenancy-model.md

This document specifies the threat model, authentication, authorization, tenancy isolation, secret handling, asset / preview link hardening, and compliance posture for theme-config-service.


1. Threat model snapshot

ThreatVectorSeverityPrimary mitigations
Cross-tenant theme leakageRLS misconfig, app bypassing app.tenant_idCriticalRLS-on by default, tenant_id enforced on every aggregate, repository tests assert isolation
Stored XSS in tenant-branded siteMalicious markup in ContentBlock.body (HTML or markdown)Criticaldompurify allow-list at write + at render in BFFs; CSP header on the booking flow origin
Unauthorized publishAuthor with read-only role triggers publishHighRBAC theme:publish; OCC; idempotency; audit log
Preview link sprayed publiclyPreview URL leaked / brute-forcedHigh256-bit secret; SHA-256 storage; expiry; rate-limited; revocable
Asset URL pointing off-platformAuthor injects external URL bypassing file-storage-serviceMediumAsset URL allow-list (only *.melmastoon.app + signed CDN URLs accepted)
Bundle tampering at the CDN edgeCompromised GCS object replacedCriticalBundles immutable; SHA-256 stored on the publication row; CDN bundle is verified by the BFF on first read after invalidation
Spoofed event from external producerUnsigned Pub/Sub message forgedHighPub/Sub publisher IAM scoped per service; envelope producer field cross-checked against the topic's IAM publisher
AI prompt injection from a tenant-controlled stringTenant injects "ignore previous instructions" into a brand keywordMediumOrchestrator-side instruction-hierarchy enforcement; HITL gates; output schema enforced; brand keywords sanitised to ≤ 64 char alphanumeric+space
OCC race causing lost-update during publishTwo simultaneous publishesMediumOCC + partial unique index theme_publications_active_uq; MELMASTOON.THEME.PUBLISH_CONFLICT
Privilege escalation via property scopeTenant_admin in chain Phase 2 edits sibling property's themeHighProperty-scoped JWT claim enforced at the gateway and re-verified in the use case

2. Authentication

CallerMechanismIdentity carrier
Backoffice console (human)OIDC → IAM session → Bearer JWTsub = user ULID, roles[], tenantId, propertyScope[]
Booking flow / BFF (server-to-server)Workload identity → mTLS to gateway → service JWTsub = service ULID, tenantId = caller's tenant context
notification-service (read email theme)Workload identity → mTLS internal endpointsub = service; bypasses gateway
Edge worker (preview / public bundle)Anonymous; preview tokens validatedn/a
Desktop sync gatewayDevice JWT bound to deviceId + tenantId + propertyIdsub = device ULID
Pub/Sub subscribersPub/Sub IAMn/a (we are the subscriber)

JWTs are issued by iam-service; we do not verify signatures locally beyond the gateway's verification (defense-in-depth optional verify in dev mode only). The gateway strips and re-signs claims into a service-internal JWT before forwarding.


3. Authorization (RBAC)

Role catalogue used by this service (subset of platform roles):

RoleGranted permissions
tenant_adminAll theme permissions including theme:publish, theme:approve_ai, theme:rollback, theme:delete_locale
brand_ownertheme:read, theme:author, theme:publish, theme:approve_ai, theme:rollback
theme_authortheme:read, theme:author (drafts only; cannot publish or approve AI)
support_admin (platform)theme:read (cross-tenant, audited; never write)
cs_agenttheme:read only on the active publication (no draft visibility)
devicetheme:read:active_published only via desktop-sync endpoints

Permission matrix per endpoint group:

EndpointRequired permission
GET /themes, GET /themes/:id, GET /theme-versions/... (drafts)theme:read
POST /themes, PATCH /themes/:id, POST /themes/:id/locales, DELETE /themes/:id/locales/:localetheme:author
POST /themes/:id/versions, PATCH /theme-versions/:idtheme:author
POST /theme-versions/:id/preview, DELETE /theme-versions/:id/preview/:tokenIdtheme:author
POST /theme-versions/:id/publishtheme:publish
POST /themes/:id/rollbacktheme:publish
AI request endpointstheme:author
AI apply endpointstheme:approve_ai AND not the original author
GET /public/themes/:id/published.jsonnone (public)
GET /public/preview/:tokenpreview-token validation
GET /internal/email-theme/:idservice-only mTLS, role service:notification-service

Property-scoped enforcement: when theme.scope === 'property', the actor's propertyScope[] must include theme.propertyId; otherwise 403 MELMASTOON.IAM.SCOPE_MISMATCH.

Authoring vs approval separation of duties: ApplyAi*UseCase rejects actor.id === suggestion.createdBy.


4. Tenancy isolation

Per ADR-0002:

  • Single-database, RLS-enforced isolation. All tenant-scoped tables have ENABLE ROW LEVEL SECURITY + tenant_isolation policy.
  • Application sets app.tenant_id GUC on every checked-out connection in a BEGIN; SET LOCAL ... pair before any tenant-scoped query.
  • Repositories assert ctx.tenantId matches tenant_id returned by every read; mismatches throw TenantBoundaryViolatedError (ops-paged).
  • Migrations and platform reads use a separate theme_config_migrator / theme_config_ro role; the latter still has RLS on (analytics queries SET app.tenant_id per partition).
  • The test harness includes cross_tenant_isolation.spec.ts that sets app.tenant_id to tenant A then attempts to read tenant B rows; must return zero rows.

5. Asset & URL handling

  • All MediaRef.url values must match ^https://(storage\\.googleapis\\.com|cdn\\.melmastoon\\.app|<tenant-cdn-allowlist>)/.+$. Anything else → MELMASTOON.THEME.ASSET_URL_NOT_ALLOWED.
  • MediaRef.contentHash (SHA-256) must be supplied for non-public assets; absence is allowed for public CDN URLs but logged.
  • AssetIntegrityClient.validateMany is called at publish time and on the daily scanner; broken URLs do not block publish only when the actor presents theme:publish_allow_broken_assets (admin override; rare).
  • The published bundle JSON contains absolute HTTPS URLs only; relative URLs are rejected at validation.

The preview link is the highest-risk surface in this service because it grants anonymous read of an unpublished theme version (which can include drafty AI-generated content).

PropertyValue
Secret length32 bytes (256 bits) from crypto.randomBytes
Encoding in URLbase64url(tokenId + ':' + secret) (token id is exposed; secret never persisted)
Storagepreview_tokens.token_hash = sha256_hex(secret) only
Comparisonconstant-time (crypto.timingSafeEqual)
TTLauthor-chosen 1–168 h (default 24 h); hard cap 168 h
RevocationDELETE endpoint or automatic on publish/draft-edit
Rate limit60 rpm per tokenHash; 600 rpm per tenant; brute-force prevented
Auditlast_accessed_at, access_count updated; desktop.theme.preview.accessed.v1 emitted with referrer header (best-effort)
Headers on responseCache-Control: private, no-store, X-Robots-Tag: noindex, no Set-Cookie
WatermarkPreview bundle response carries meta.preview = true; the booking flow renders a top banner "PREVIEW — not yet published"

Even with the secret, the preview token gives read access only to one specific draft version, never to other versions, other themes, or other tenants.


7. CDN bundle integrity

  • Bundles are uploaded to GCS with gsutil_cors-restricted, signed URLs are not used (the bundle is public). Object generation is captured; Cache-Control: public, max-age=300, s-maxage=86400, stale-while-revalidate=60.
  • The publication row stores bundle_sha256. The BFF, on first cache-miss after a CDN invalidation, fetches the bundle, recomputes SHA-256, and refuses to serve if mismatched (raises theme.bundle.integrity_violation.v1 + ops page).
  • GCS bucket has Object Versioning + retention policy 30 days to protect against accidental overwrite.

8. Secrets & key management

SecretStorageRotation
Cloud SQL connection (IAM auth)Workload Identity, no static secretn/a
Pub/Sub publish IAMWorkload Identityn/a
GCS bucket accessWorkload Identityn/a
Memorystore AUTHSecret Manager → mounted env at boot90 days
AI orchestrator mTLSWorkload Identity certs from Workload Identity Federationrotated daily by GCP
Preview token secretsnever stored at restper-token (created → user-shared)
HMAC for desktop pairing JWT verificationSecret Manager90 days

The service has no static API keys. All secrets that exist are GCP-managed and short-lived.


9. Input validation & safety

  • All MarkupEntry format: 'html' and format: 'markdown' content is sanitised through dompurify configured with the MELMASTOON_THEME_SAFE_HTML allow-list (no <script>, no <iframe> except YouTube/Vimeo, no inline event handlers, no javascript: URLs, attribute allow-list per tag).
  • ContentBlock.meta.layout and similar enums are validated against fixed allow-lists.
  • LocalePack.entries ICU strings parsed by @formatjs/icu-messageformat-parser; invalid syntax → 400 LOCALE_PLACEHOLDER_MISMATCH with field-level violation.
  • MediaRef.url validated against the asset URL allow-list (§5).
  • NavigationItem.target validated against a route allow-list when kind === 'route'; https:// only when kind === 'external'; tel: and mailto: allowed for kind === 'tel'|'email'.
  • All numeric inputs (e.g. ttlHours, ordinal, spacing scale) validated with min/max bounds.

10. Logging, audit, and PII

  • Application logs are JSON, structured, redacted: no full request bodies, no full prompts, no token secrets.
  • Audit events flow to audit-service via theme.audit.* events; immutable, 7-year retention.
  • PII handling: this service generally does not process PII. The exceptions are:
    • actor.id, actor.ip, actor.userAgent on commands → audit-only, never indexed by tenant_id.
    • Author/approver names appear in HITL surfaces; not stored, fetched on demand from iam-service.
  • DSAR requests routed to the platform DSAR pipeline; we contribute audit log entries scoped to the user.

11. Compliance posture

RequirementStatus
GDPR data minimisationEnforced at AI boundary (§AI_INTEGRATION §4)
GDPR right to erasureTenant deletion cascades to soft-delete + 30-day hard purge; AI logs purged on schedule
WCAG 2.1 AAHard-enforced on token contrast at publish
OWASP ASVS L2Mapped in services/theme-config-service/security/ASVS_MAPPING.md
ISO 27001 controlsInherited from platform; service-specific controls listed in services/theme-config-service/security/SOC2_CONTROLS.md
Pen-test cadenceQuarterly authoring surface; annual full scope
Dependency scanningSnyk + GitHub Advanced Security in CI; high-severity blocks merge
Container scanningTrivy on every image build; CIS Docker baseline
Secret scanningPre-commit + GitHub secret scanning

12. Incident response runbooks (pointers)

IncidentRunbook
Cross-tenant data exposure suspectedrunbooks/sev1-cross-tenant.md
Stored XSS in tenant contentrunbooks/sev1-stored-xss.md
Bundle integrity violationrunbooks/sev2-bundle-integrity.md
Preview link suspected leakedrunbooks/sev3-preview-leak.md (revoke all tokens for the version + rotate)
AI prompt injection observedrunbooks/sev3-ai-prompt-injection.md
RLS bypass observedrunbooks/sev1-rls-bypass.md

Runbooks live in the monorepo and are exercised quarterly.


13. References