07 — Security, Compliance & Multi-Tenant Isolation
Companion: 02 Enterprise Architecture · 03 Microservices · 05 API Design · 06 Data Models · 08 AI Architecture · 09 Lock & Key Integration · 10 Payments Architecture · 12 Desktop Spec · ADR-0002 Multi-Tenancy · ADR-0003 Electron Offline-First
This document is the canonical security, compliance, and tenancy enforcement reference for Ghasi Melmastoon. Every other document defers to this one for the rules. The approach is defense in depth at every layer — domain, application, database, network, edge, client — with the assumption that any single layer can fail and the others must hold.
1. Threat Model (STRIDE per surface)
We apply STRIDE to each major surface separately because each has a different audience, attack profile, and blast radius.
1.1 Consumer meta layer (bff-consumer-service + Next.js meta site + React Native consumer app)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing | Bot scraping availability + price | Anonymous rate limiting at Cloud Armor + Kong; CAPTCHA on suspicious traffic; aggressive caching to absorb scrape load |
| Tampering | Search payload manipulation to forge low prices | Server is the only source of truth; client-supplied price in payload is ignored; price re-derived per request |
| Repudiation | Anonymous booking attempts repeatedly with no audit | Every search + booking-intent emits an event with anonymous session id + IP fingerprint; rate-limited by (ip, ua, session) |
| Information disclosure | Cross-tenant data leak in meta search | Meta layer only reads from search-aggregation-service projection — no PII, no payment data, no internal financials in that projection |
| Denial of service | Map-bound query flood | Bounding-box queries are capped at zoom level + max-results; results capped per tenant |
| Elevation of privilege | Anonymous → authenticated bypass | No authenticated routes on the meta surface; all booking-protected actions require a tenant booking session |
1.2 Tenant booking surface (bff-tenant-booking-service + tenant booking site + consumer app per-tenant flow)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing | Pretend to be a different tenant via header manipulation | Tenant resolved server-side from URL/slug + signed booking session; never from client header |
| Tampering | Modify quote between display and pay | Quote signed (HMAC) with server-side TTL; payment refuses if signature/quote mismatched |
| Repudiation | Guest claims they didn't book | Booking session + signed quote + payment provider receipt + audit chain |
| Information disclosure | Other-tenant theme/policy leakage | Tenant booking BFF resolves tenant once per request and refuses cross-tenant fetches in the same request |
| Denial of service | Hold-and-abandon flooding inventory | Hold TTL 10 min; per-IP rate limit; abandoned-hold reaper |
| Elevation of privilege | Guest → staff via JWT manipulation | JWT is signed (RS256); tenant role claims verified per request; tenant booking surface never accepts staff JWTs |
1.3 Backoffice Electron desktop (bff-backoffice-service + Electron app)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing | Unpaired/imposter device sync push | Refresh token bound to device public key registered at pairing; mismatched key → 401 + re-pair |
| Tampering | Local DB extraction from a lost laptop | SQLCipher with device-derived key; OS keychain (keytar) holds the key fragment; user passphrase optional second factor |
| Repudiation | Staff denies a folio adjustment | Append-only audit_events per service + central BigQuery + daily Merkle anchor |
| Information disclosure | Renderer XSS exfiltrating data | nodeIntegration: false, contextIsolation: true, narrow contextBridge API; CSP default-src 'self'; remote module disabled |
| Denial of service | Sync flood from a buggy or compromised client | Per-device rate limits at the sync surface; max-batch enforced; revoke device on policy breach |
| Elevation of privilege | Cashier escalates to GM-only refund | RBAC + ABAC enforced server-side; UI hides but server checks; refunds above policy threshold require a second authorizer |
1.4 Mobile consumer app (React Native)
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing | Stolen device session | Refresh token in OS keychain; pinned cert; force-reauth on root/jailbreak detection (best-effort) |
| Tampering | API client modified to bypass policy | Server-only enforcement; client policy is UX, not security |
| Repudiation | Booking dispute | Same provenance trail as the tenant booking surface |
| Information disclosure | Plaintext storage of guest data | Encrypted async storage for any cached PII; never store payment PAN |
| Denial of service | App-level abuse | Backed by Cloud Armor + tenant booking BFF rate limits |
| Elevation of privilege | Fake admin endpoints | The app talks only to BFFs; no admin endpoints exist on the consumer BFF |
1.5 Internal services + east-west traffic
| STRIDE | Threat | Mitigation |
|---|---|---|
| Spoofing | Service A pretends to be Service B | Service-to-service mTLS where supported; otherwise short-lived service-account JWTs verified at the receiver |
| Tampering | Pub/Sub event payload tampering at rest | Cloud Pub/Sub at-rest encryption + CMEK on PII topics; payload schema validated by consumer; dead-lettered if invalid |
| Repudiation | Service mutates without trace | Outbox pattern + audit emit; dependency-graph CI fails any mutation without a paired audit emit |
| Information disclosure | Logs leak PII | pino redactors strip declared PII keys at serialization; CI scans for new fields without redaction |
| Denial of service | Cascading retry storms | Per-port circuit breakers; exponential backoff with jitter; per-request fail-fast budget |
| Elevation of privilege | Service account misuse | Per-service service account with least privilege; KMS keys scoped per service; ops bypass requires JIT elevation |
1.6 Top-of-list cross-cutting threats
- Cross-tenant data leak — a single missed
WHERE tenant_id = ?clause becomes a breach. Mitigation: RLS on every table +tenant_idvalue object in domain + outboxtenantIdassertion + integration "two-tenant simulator" tests on every service. - Lock credential theft — vendor credentials are the keys to physical doors. Mitigation: dedicated KMS key, isolated namespace inside
lock-integration-service, no other service has read access, never logged, audit chain immutable. - Payment fraud — synthetic accounts and chargebacks. Mitigation: PCI scope minimization (Stripe Elements / PayPal SDK tokenize client-side), AI anomaly detection on payment patterns with HITL gates for auto-block.
- AI prompt injection — guest-supplied content reaches the model and exfiltrates secrets. Mitigation: input length cap, system prompt isolation, output schema validation, PII redaction, output moderation.
- Offline DB extraction from lost laptop — Electron SQLite stolen. Mitigation: SQLCipher; device-derived key; remote-revoke on next sync attempt; minimal data scope per device.
- Supply-chain compromise (npm) — malicious package update. Mitigation: lockfiles + Dependabot + Snyk + SLSA provenance attestations + container scan + signed commits + signed-tag releases.
2. Authentication Architecture
2.1 JWT (primary)
- Algorithm: RS256 (asymmetric); JWKS published from
iam-serviceat/.well-known/jwks.json. Key ID (kid) rotation quarterly with grace overlap. - Access token: 15 minutes lifetime. Carries
sub(UserId),tenant_id,roles,property_ids,device_id,iat,exp,jti. - Refresh token: 30 days, rotating (single-use rotation). Family-revoke on detected reuse — if a refresh token is presented after it has already been rotated once, the entire chain is revoked and the user is forced to re-authenticate.
- Refresh on Electron + mobile: refresh token is bound to the device public key registered at pairing. Refresh request signs a challenge with the device private key (held in OS keychain via
keytar); the IAM service rejects refreshes whose signature does not verify against the bound public key. Stolen refresh tokens without the device key are useless. - Storage: access token in process memory only (never localStorage); refresh token in OS keychain on Electron + mobile, in
httpOnly; Secure; SameSite=Strictcookie on the web.
2.2 OIDC / SAML SSO (chain operators)
- Phase 2 capability: OIDC + SAML 2.0 federation for chain operators with their own IdPs (Auth0, Keycloak, Okta, Azure AD, Google Workspace).
- Implemented in
iam-servicevia thepassport-oidcandpassport-samlstrategies behind a singleExternalIdentityProviderport. - Per-tenant provider configuration;
iam-serviceprovisions a localUserand aMembershipon first login. - SCIM 2.0 endpoint (Phase 3) for centralized user provisioning from chain HRIS.
2.3 WebAuthn / Passkeys (Phase 2)
- Optional second factor (or sole factor for staff with hardware keys). FIDO2 level 2.
- Public keys persisted on
users.webauthn_credentials; private keys never leave the user's device. - Backoffice users can be required by tenant policy to enroll a passkey for refund + lock-credential operations (step-up auth).
2.4 Magic link (guest-side)
- Guests booking on the tenant site can opt into an account by receiving a one-time magic link (
iam-serviceissues,notification-servicedelivers via email/SMS). - Magic link is a 30-minute single-use token; on click,
iam-serviceissues a normal access + refresh pair. - Used to access booking history, modify reservations, and receive digital keys without a password.
2.5 Service-to-service
- Cloud Run services authenticate to each other via short-lived Google service-account ID tokens verified at the receiver (or mTLS where the topology supports it).
- No service-to-service traffic uses static API keys.
3. Authorization Architecture
3.1 RBAC roles (canonical)
Roles are coarse, hierarchical, and tenant-scoped (except platform roles). The canonical list:
| Role | Scope | Typical permissions |
|---|---|---|
platform.super_admin | Platform | Tenant lifecycle; emergency support; ops elevation. Audited. |
platform.support | Platform | Read-only across tenants for support tickets, with explicit per-ticket elevation + audit |
platform.compliance_officer | Platform | DSAR fulfilment; audit log read; AI provenance review |
tenant.owner | Tenant | All tenant operations including billing, property creation, plan upgrades, member invites |
tenant.gm | Tenant (or property) | All operational + reporting; cannot change billing/plan |
tenant.front_desk | Property | Reservations, check-in/out, folio operations within policy, key issuance |
tenant.housekeeping_lead | Property | Manage HK tasks, assignments, room status overrides |
tenant.housekeeping | Property | Update assigned tasks; flip room status; raise flags |
tenant.maintenance | Property | Triage and close maintenance tickets |
tenant.finance | Tenant | Reconciliation, invoices, refunds above threshold (with approval), tax exports |
tenant.marketing | Tenant | Theme + content blocks; promotion + rate-plan creation |
chain.operator | Cross-tenant within a chain account | Read across owned tenants; cross-property reporting; cannot mutate ops directly |
guest | Self | Own reservations, own folio, own profile |
3.2 ABAC attributes
Beyond role checks, fine-grained access uses ABAC predicates evaluated by the policy engine. Attributes:
tenant_id— must match for any tenant-scoped resource.property_id— staff scoped to one property within a multi-property tenant cannot read other properties.data_residency— request region must satisfy the tenant's residency pin; cross-region requests rejected with explicit error.step_up_recent— boolean; some actions (refunds above threshold, lock credential bulk revoke) require a recent passkey/WebAuthn step-up within the last 5 minutes.time_of_day— optional per-role policy that limits sensitive actions to business hours.
Predicate examples:
resource.tenant_id == ctx.tenant_id
resource.property_id IN ctx.user.property_ids
resource.amount_micro <= role.refund_threshold_micro OR ctx.step_up_recent
ctx.region IN tenant.residency_allowed_regions
3.3 Decision flow
- JWT verified at Kong → claims loaded.
bff-*-serviceresolves the route's required(resource:action).- Policy engine evaluates
(role grants action) AND (all ABAC predicates true). - Decision logged with
decisionId; UI may pre-check viaPOST /api/v1/authz/checkto avoid showing forbidden actions. - Domain layer re-checks the
tenantIdinvariant on every aggregate operation as a final defense.
4. Tenant Isolation at Every Layer
| Layer | Mechanism | What it catches | What happens on breach |
|---|---|---|---|
| Domain | Aggregates carry tenantId: TenantId; cross-aggregate refs validated at construction | Wrong tenant id passed in a use-case parameter | CrossTenantReferenceError thrown before persistence; 403 returned |
| Application (use-case) | Use-cases require explicit TenantId parameter; not implicit via ALS | Forgetting to pass tenant context | Type-checker rejects the call site; CI fails |
| API middleware | Request-scoped middleware sets app.tenant_id from JWT claim before any DB call | Header spoofing (X-Tenant-Id differs from JWT) | 403 at the gateway; audit log entry |
| Database (RLS) | tenant_id column + FORCE ROW LEVEL SECURITY policy USING (tenant_id = current_setting('app.tenant_id')::uuid) | Bug in app code that omits WHERE tenant_id = ? | Query returns 0 rows; insert with wrong tenant_id throws RLS violation |
| Connection pool | PgBouncer transaction-mode init script sets app.tenant_id per checkout | Connection reuse across tenants | app.tenant_id cannot be unset between checkouts |
| Outbox | Writer asserts payload.tenantId == current_setting('app.tenant_id') before commit | Wrong tenant in event payload | Aborted transaction; alert |
| Pub/Sub envelope | tenantId in envelope; consumer asserts on receive | Cross-tenant event bleed via mis-routing | Event dropped; DLQ; alert |
| Search projection | search-aggregation-service is the only cross-tenant store; PII excluded by projection schema | Accidental PII inclusion in projection | CI fails the projection schema diff |
| Storage | Cloud Storage objects under gs://melmastoon-<env>-tenant/<tenantId>/...; signed URLs are scoped to that prefix and the requesting caller | Cross-tenant URL guessing | 403 at signed URL validation |
| AI | Per-tenant prompt namespace; per-tenant vector namespace (RLS on pgvector tables); per-tenant model cache key | One tenant's RAG context bleeding into another's prompt | Predicate failure → no rows returned; explicit alert |
4.1 Two-tenant simulator (CI requirement)
Every service must include an integration test suite that:
- Provisions two tenants
AandB. - Inserts data via the public API for both.
- Asserts that every read endpoint, with
A's JWT, returns onlyA's data, and vice versa. - Asserts that every write endpoint refuses cross-tenant references (
A's JWT writing toB's resource → 403/CrossTenantReferenceError).
A new service ships only after this suite is green.
5. Secrets Management
5.1 Storage
- All secrets (provider keys, signing keys, lock vendor credentials, payment provider keys, AI provider keys, JWT signing keys) live in Google Secret Manager.
- Never in env files committed to git. Never in client bundles. Never in CI logs (CI uses workload identity federation; secrets are mounted to runners just-in-time and redacted).
5.2 Service accounts
- Per-service service account with the minimum set of secret accessors required.
- Lock vendor credentials live in dedicated secrets named
secret-lock-<vendor>(e.g.,secret-lock-ttlock,secret-lock-salto). Only thelock-integration-serviceservice account can read them. No other service has accessor permission. - Payment provider keys live in
secret-payment-<provider>(e.g.,secret-payment-paypal,secret-payment-stripe). Onlypayment-gateway-servicecan read.
5.3 Envelope encryption
- KMS-wrapped DEKs for application-level field encryption. Each per-record DEK is generated once, wrapped under the service's KMS key, persisted alongside the ciphertext, and unwrapped only on read in process memory.
- KMS keys are environment-scoped and never shared across environments.
5.4 Rotation
- Quarterly rotation policy for application-level secrets (lock keys, payment keys, AI keys).
- Annual rotation for KEKs (with grace window — old DEKs still unwrap during the window; background re-encryption job rolls records onto the new key).
- Immediate rotation on suspected compromise (incident response runbook).
- Rotation is a one-button operation in
iam-service's ops console; the saga handles distribution and verification.
5.5 Audit
- Every secret access is logged to Cloud Logging. Anomaly detection alerts on unexpected access patterns (out-of-hours, by an unexpected service account, from an unexpected region).
6. Payment Security
6.1 PCI scope
We deliberately keep PCI scope minimal:
- Cardholder data never touches our servers. Stripe Elements / PayPal SDK / equivalent tokenize client-side; only the resulting opaque token reaches
payment-gateway-service. - Our PCI scope is SAQ A (the smallest applicable level).
payment-gateway-serviceruns in its own Cloud Run service with a dedicated service account and a dedicated KMS key.- The schema-per-tenant carve-out (
tenant_<uuid>_payments) provides structural isolation per tenant; cross-tenant payment-data leakage is impossible inside Postgres.
6.2 Token storage
- Stored: provider token + provider reference + amount + currency + status + metadata (PII-redacted).
- Never stored: PAN, CVV, expiration, cardholder name (we display the last-4 from the tokenized response only when the provider returns it).
6.3 Cash on arrival
- Cash flow lives entirely inside
billing-service(schema-per-tenant) andpayment-gateway-servicecashrail. No card data involved. - Cash payments require
metadata.received_by(StaffId) andmetadata.locationfor reconciliation. - Daily reconciliation report compares
payments.cashrows to staff cash drawer counts; deviations raiseaudit.cash.deviation.v1.
6.4 Refund authorization
- Refunds above a configurable per-tenant threshold (default: 50,000 micro AFN-equivalent USD) require a second authorizer with
tenant.financeortenant.gmrole and a step-up auth (passkey or fresh password) within the last 5 minutes.
6.5 Webhook integrity
- Provider webhooks (Stripe, PayPal) are HMAC-signed and timestamped. We verify both signature and timestamp window (5 minutes); replays are detected via nonce store in Memorystore.
7. Lock & Key Security
7.1 Vendor credential isolation
- All vendor credentials live in Secret Manager under
secret-lock-<vendor>, accessible only bylock-integration-service's service account. - Vendor credentials are loaded once at startup into process memory and never logged. CI fails any log statement that references a known credential variable name.
7.2 Key issuance audit chain
- Every lifecycle event (
issue,update,revoke,suspend) is appended tokey_credentials.audit_chain(see 06 Data Models §4.11). - The chain is append-only at the SQL level (no UPDATE / DELETE).
- Each entry carries
(at, actor, event, vendorRef); the chain is replicated toaudit-service's BigQuery dataset.
7.3 Revoke-on-checkout (mandatory)
reservation.checkout.v1is not considered final untillock.key.revoked.v1is observed for every credential issued for that reservation.- If the vendor is unreachable, the revoke is queued; the reservation enters
checkout_pending_revokesubstate; the front desk is alerted.
7.4 Lost-key flow
- Front desk staff trigger
key.lostfrom the desktop app. The use-case:- Revokes the lost credential immediately.
- Issues a new credential with the same
validFrom/validTo. - Logs both events on the audit chain with
revoke_reason='lost'. - Notifies the guest by SMS/email.
- Failed revocation paths surface a high-priority alert; the room may be flipped to
oountil resolved.
7.5 Offline key issuance
- The desktop app can request key issuance while offline only for vendors that support offline credential generation (e.g., TTLock OfflinePassword API).
- The credential is generated using a tenant-scoped key derivation chain seeded from the property's
lockSeedprovisioned at vendor onboarding. The seed is sealed in the OS keychain and is never written to disk. - The local issuance event is queued in the outbox; on next sync, the cloud
lock-integration-servicereconciles, registers the credential, and emitslock.key.issued.v1.
8. AI Safety
8.1 Prompt injection mitigations
- Input length cap per use case (e.g., 4 KB for guest-facing chat; 16 KB for admin-side analysis). Truncation with explicit notification to the model.
- System prompt isolation — the system prompt is injected by
ai-orchestrator-servicefrom the prompt registry; it is never composed from user input. Prompt templates are stored separately from runtime variables. - Output schema validation — every model response is validated against a JSON schema declared by the prompt template. Non-conforming outputs are rejected and either retried (once) or returned as
nullwithMELMASTOON.AI.OUTPUT_SCHEMA_VIOLATION. - Tool-call isolation — the model can call only the tools declared by the use case; tool definitions are server-side and authenticated.
8.2 HITL gates (irreversible / high-impact actions)
The following AI-suggested actions must sit in draft_ai state until accepted by a user with the appropriate role:
- Auto-cancel a reservation flagged as fraudulent.
- Auto-refund any amount.
- Dynamic price publish when AI-suggested price deviates >5% from the BAR baseline.
- Anomaly-flagged booking auto-block beyond temporary hold.
- Bulk lock-credential revoke initiated by anomaly detection.
- Guest-facing message dispatch drafted by the model.
Acceptance is recorded as a Decision (dec_…) with (actor, at, reason) and linked to the resulting state-change event.
8.3 Content moderation
- All guest-facing AI output passes through the post-moderation step: profanity, dangerous content, PII, prompt-leak markers.
- Failed moderation → output blocked, fallback to deterministic template (e.g., a canned acknowledgement), incident raised if classification is "harmful".
8.4 Provenance metadata (mandatory)
Every AI artifact persists with the AIProvenance envelope (see 02 Enterprise Architecture §9.3 and 08 AI Architecture §6). No AI artifact may be displayed to a user without provenance metadata; the UI surfaces an "AI" badge with a click-through to provenance details.
8.5 PII redaction
- Before any cloud-bound payload,
ai-orchestrator-serviceruns a PII-redaction pass: emails, phones, government IDs, credit-card-shaped strings, IBANs. - Redaction emits
ai.redaction.applied.v1with counts (no content) for ops dashboards.
9. Compliance Posture
| Standard / Regulation | Posture | Evidence |
|---|---|---|
| GDPR | Full data subject rights (access, erasure, portability, rectification, restriction); DPIA template per processing activity; 72-hour breach notification runbook; DPO role; lawful-basis register | audit-service DSAR saga; DPIA template under docs/standards/; breach response in §13 |
| Afghanistan / regional data residency | Per-tenant residency pin (me-central1 / europe-west1); data plane never crosses pinned region without explicit opt-in; logs locale-tagged | tenant-service settings; egress audited |
| PCI-DSS | SAQ A scope; cardholder data never on our servers; quarterly ASV scan on the booking + payment surfaces; annual SAQ self-assessment | Stripe Elements / PayPal SDK; payment-gateway-service schema-per-tenant; ASV reports |
| SOC 2 Type II | Phase 3 target. Continuous controls (access reviews, change management, vulnerability management, incident response). Observability + audit log are SOC-2-aligned from day 1 | Roadmap §Phase 3; control catalog under docs/compliance/ (Phase 3 deliverable) |
| ISO 27001 | Phase 4 target. ISMS scope, risk register, control statement of applicability | Risk register under docs/risks/; ISMS docs (Phase 4) |
| WCAG 2.2 AA | Full AA on all guest-facing surfaces; AA on critical operational paths in the desktop app | Accessibility CI checks; periodic audit |
10. Data Residency
- Primary region:
me-central1(Doha) when available for the relevant Cloud SQL / Pub/Sub / Cloud Run feature set; otherwiseeurope-west1(Belgium) as the closest practical region serving Afghanistan with full feature parity. - Per-tenant region pin (Plus + Enterprise plans, Phase 3): tenant data is stored in the pinned region; data plane operations refuse to cross the pin.
- Cross-region replication: off by default; enabled only with explicit tenant opt-in.
- Backups stay in the same region as the primary; cross-region DR is opt-in and additionally KMS-wrapped.
- CDN: static assets and theme tokens are cacheable globally (no PII), so CDN edges may serve any region.
- Logs: Cloud Logging is region-pinned per project;
tenant_idtagged on every line; per-tenant retention pins in Plus + Enterprise.
11. Audit & Forensics
11.1 Append-only logs
- Every service writes to its local
audit_eventstable (append-only, see 06 Data Models §9). - Every row is also published to Pub/Sub and lands in the central BigQuery
melmastoon_audit_logdataset.
11.2 Retention
| Class | Retention | Storage |
|---|---|---|
| Authentication events | 2 years | BigQuery + Cloud Storage Coldline archive |
| Reservations + housekeeping events | 3 years | BigQuery + tenant-scoped archive |
| Financial events (folio, payment, refund) | 7 years (regulatory) | BigQuery melmastoon_audit_log_financial + Cloud Storage immutable bucket (object lock) |
| AI provenance | 7 years | BigQuery melmastoon_audit_log_ai |
| Lock-credential lifecycle | 5 years | BigQuery + immutable bucket |
| DSAR fulfilment proofs | 10 years | Immutable bucket |
11.3 Tamper evidence
- Daily Merkle root computed over yesterday's audit events; anchored to a public RFC 3161 timestamp authority. Anchors persisted in
melmastoon_audit_anchors. - Quarterly verification job re-derives roots and checks anchors.
11.4 Forensic readiness
- Incident timeline reconstruction is supported by
trace_idcorrelation across services + audit events + Pub/Sub message archive (7 days default; tunable). - Full request replay possible from
request_idfor non-mutating reads.
12. Vulnerability Management
12.1 Static + supply chain
- Snyk + GitHub Dependabot for npm dependencies. PRs blocked on critical vulnerabilities; high vulnerabilities require explicit acceptance with a remediation plan.
- Lockfiles (
package-lock.json/pnpm-lock.yaml) committed. CI verifies--frozen-lockfile. - SBOM generated per build (CycloneDX); SLSA provenance attestations for built artifacts.
- SAST via CodeQL on every PR; secret scanning at the GitHub level + locally via
gitleakspre-commit hook.
12.2 Container scanning
- All container images built into Artifact Registry are scanned by Container Analysis. Critical CVEs block deploy; the deploy gate consumes the scan result before promotion.
12.3 Pen test + bug bounty
- Quarterly external penetration test on the public surfaces (consumer meta, tenant booking, mobile, the Electron auto-update channel).
- Bug bounty opens in Phase 3 with scoped surfaces and reward tiers.
12.4 IaC + change review
- Terraform for all GCP infra;
terraform planposted as PR comment; required reviewer for the security-sensitive resources (KMS keys, IAM bindings, Secret Manager). - Pre-merge
tflint+tfsecchecks.
13. Incident Response
13.1 Severity classes
| Severity | Examples | SLA |
|---|---|---|
| Sev 1 | Confirmed cross-tenant data leak; payment data exposure; lock-credential exposure; consumer surface fully down >5 min | Page on-call within 5 min; communications + remediation continuous |
| Sev 2 | Single-tenant data exposure; backoffice down for one tenant; payment failure rate >10% | Page on-call within 15 min; remediation within 4 hours |
| Sev 3 | Single property impacted; degraded AI; sync lag > 30 min for one device fleet | Notify on-call within 1 hour; remediation within 24 hours |
| Sev 4 | Cosmetic, single-user impact, recoverable workaround | Tracked in next sprint |
13.2 On-call rotation
- 24/7 primary + secondary rotation across regions during business growth.
- Escalation path documented in PagerDuty (or equivalent); each service has a designated on-call owner.
- Runbook links live in
docs/observability/runbooks/.
13.3 Communication templates
- Internal status update template (every 30 min during a Sev 1).
- Tenant-facing notice template (translated into supported locales).
- GDPR breach notification template (72-hour deadline tracking).
13.4 Post-incident review
- Mandatory blameless PIR within 5 business days for Sev 1/2.
- Output: timeline, root cause, contributing factors, action items with owners + dates, and a "what would prevent this entirely" section.
- Action items tracked to closure in the engineering backlog with the
pir-followuplabel.
14. Secure SDLC
| Control | How |
|---|---|
| Branch protection | develop and main protected; PRs require ≥1 review + green CI; force-push disabled |
| Signed commits | Required on main for ops + release engineers; recommended elsewhere; gpg or Sigstore-based |
| Mandatory review | Code review by someone other than the author; security-sensitive paths (auth, payment, lock, AI gateway) require a security reviewer in the CODEOWNERS |
| SAST | CodeQL on every PR; Semgrep rules tuned per service for known anti-patterns (e.g., raw child_process in renderer, missing tenant_id, string-concatenated SQL) |
| Secret scan | gitleaks pre-commit + GitHub secret scanning + push protection |
| IaC review | Terraform changes reviewed by SRE + security; tfsec blocks on high-severity findings |
| Change advisory for production | Production deploys require a CHANGE record in the deploy log, including risk assessment, rollback plan, and observability checkpoints |
| Two-person rule for sensitive ops | Tenant erasure, KMS key rotation, financial schema migration require two operators |
15. Electron Security Hardening
The Electron desktop is the highest-leverage attack surface — it holds offline data, has Node-level capability, and is widely deployed on staff laptops. We harden it aggressively. See ADR-0003 for the full rationale.
15.1 Process model
nodeIntegration: falseon every renderer. The renderer is plain Chromium; it cannotrequire('child_process').contextIsolation: true. The renderer's JS context is isolated from preload's. Preload exposes only whatcontextBridgedeclares.sandbox: trueon every BrowserWindow. The renderer process is a sandboxed Chromium child.webSecurity: true(default; never disabled).allowRunningInsecureContent: false.enableRemoteModule: false(theremotemodule is gone in modern Electron; we explicitly verify it is not re-added).
15.2 contextBridge API surface (narrow)
window.melmastoon is the only object exposed to the renderer. It exposes a small, typed surface:
window.melmastoon = {
api: {
request: (route: string, payload: unknown) => Promise<unknown>, // typed routes only
},
sync: {
pullNow: () => Promise<SyncSummary>,
pushNow: () => Promise<SyncSummary>,
onStatus: (cb: (s: SyncStatus) => void) => Unsubscribe,
},
ai: {
infer: (capability: EdgeCapability, input: unknown) => Promise<unknown>,
},
keys: {
issueOffline: (req: OfflineKeyRequest) => Promise<KeyCredentialDraft>,
onCredentialUpdate: (cb: (e: KeyCredentialEvent) => void) => Unsubscribe,
},
printer: { /* receipt-printer integration */ },
app: { version: string, locale: string },
};
No fs, no child_process, no eval, no arbitrary IPC channel — all renderer requests go through api.request(route, payload) which is dispatched to a switch in the main process; routes that the main process does not recognize are logged and rejected.
15.3 CSP
The renderer enforces a strict CSP via the <meta http-equiv="Content-Security-Policy"> tag and via Electron's webRequest.onHeadersReceived so it cannot be removed at runtime:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline'; // Tailwind generates inline styles in dev; tightened in prod build
img-src 'self' data: https://storage.googleapis.com https://cdn.melmastoon.app;
connect-src 'self' https://api.melmastoon.app https://sync.melmastoon.app;
font-src 'self' data:;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'none';
15.4 Auto-update integrity
- Built with
electron-builder; updates served viaelectron-updater. - Code signing mandatory:
- Windows: EV code-signing certificate; SmartScreen reputation maintained.
- macOS: Apple Developer ID + notarization.
- Linux (AppImage): GPG-signed; signature verified on update.
electron-updaterverifies the publisher signature on every update before applying. A signature mismatch refuses the update and logs to telemetry.
15.5 Token + secret storage
- Refresh token + device key fragment stored in OS keychain via
keytar(Credential Manageron Windows,Keychainon macOS,Secret Serviceon Linux). - Plain-text tokens are never written to disk.
- Process memory carrying secrets is zeroed after use where the language allows; we do not over-promise here (Node + V8 do not give precise control), but we minimize the surface.
15.6 Local DB encryption
- SQLite database file (
melmastoon.db) is encrypted with SQLCipher (AES-256-CBC). Key is derived from(deviceKey ⊕ userPassphrase?); the deviceKey is generated at first install and held inkeytar. - Key derivation parameters:
kdf_iter = 256000,cipher_page_size = 4096. - Loss of
keytarentry forces re-pair (no recovery — intentional). The user re-pairs from a fresh login + device-binding flow.
15.7 Other hardening
- Disable Node integration in subframes (
nodeIntegrationInSubFrames: false). - Restrict navigation —
will-navigateandnew-windowhandlers refuse navigation to anything outside the allowlist (api.melmastoon.app, sync.melmastoon.app, our own help URLs). - Disable DevTools in production builds;
webContents.on('devtools-opened', () => webContents.closeDevTools())as a hard guard. - Crash reporter scrubs paths + tokens + tenant ids from crash dumps before upload.
- Telemetry is opt-in, anonymized, and never includes user-identifiable content.
Cross-references: per-service security details live in
services/<service-name>/SECURITY_MODEL.md. Lock-specific deep dive in 09 Lock & Key Integration. Payments deep dive in 10 Payments Architecture. AI safety deep dive in 08 AI Architecture.