SECURITY_MODEL — lock-integration-service
Bundle: SERVICE_OVERVIEW · DATA_MODEL · API_CONTRACTS · APPLICATION_LOGIC · SYNC_CONTRACT · OBSERVABILITY
Cross-cutting: docs/07 — Security, Compliance, Tenancy, docs/architecture/ADR-0004 §Security, docs/09 §11 Security and audit.
This service controls physical access to rooms. Compromise here costs guests, staff, and reputation. The threat model is correspondingly aggressive and the controls are stricter than for most microservices.
1. Threat model
1.1 Assets
| Asset | Sensitivity | Reason |
|---|---|---|
vendor_credentials.secret_ref (vendor API keys, OAuth refresh tokens, on-prem connector mTLS certs) | CRITICAL | Exfiltration → attacker can mint keys for any room of any property using that vendor account |
OfflineIssuance.private_key (Ed25519 private key on the desktop OS keychain) | CRITICAL | Forge offline-issued credentials at any property where the cert is trusted |
KeyCredential.vendorRef (encoder card serials, mobile token IDs) | HIGH | Replay/clone of a single guest credential |
MasterKey.vendorRef | CRITICAL | Whole-property access |
lock_audit rows | HIGH | Tampering hides incidents from regulators and the GM |
key_credential_attempts rows | MEDIUM | Privacy: who entered which room when |
lock_devices.vendor_device_ref | MEDIUM | Lateral movement to vendor portal |
| Webhook ingress traffic | HIGH | Spoofed vendor webhooks → forced state on credentials |
1.2 Adversaries (per 07 §3)
- External attacker with internet access (webhook spoofing, REST endpoint abuse).
- Malicious tenant operator (cross-tenant pivot, vendor-cred exfiltration).
- Malicious staff (master-key abuse off-shift, attempt-log scrubbing).
- Compromised desktop (stolen Ed25519 key → offline forgery).
- Vendor compromise (vendor cloud account stolen → mass credential issuance).
2. Identity, authentication, authorization
2.1 Service-level
- All inbound REST traffic is fronted by API Gateway (Cloud Endpoints) which enforces OAuth 2.0 Bearer tokens minted by
iam-service. Tokens carrysub,tenant_id,roles[],device_id?,scopes[],aud="api://lock-integration". - Service-to-service inbound (e.g., from
sync-service) uses GCP Workload Identity + signed JWTs withaud="svc://lock-integration". - Service-to-service outbound to vendor APIs uses credentials fetched from Secret Manager at request time; never cached on disk; in-memory cache TTL ≤ 60 s.
2.2 Tenant scoping
Every authenticated request asserts X-Tenant-Id header. The middleware:
- Asserts header
tenant_id∈ token'stenant_ids[](multi-tenant operators can hold many). - Sets
app.tenant_idPostgres GUC at the start of the transaction; all queries flow through RLS policies ontenant_id. - On RLS violation (no rows returned from a query that should have returned at least one because
tenant_idwas wrong), returnsMELMASTOON.IAM.TENANT_MISMATCH(403) — never silently empty results.
2.3 Role-based authorization
| Role | Can call |
|---|---|
system_admin | All endpoints (cross-tenant for support; audit-logged) |
tenant_admin | All endpoints scoped to their tenant; vendor-adapter CRUD |
property_manager | Key credential CRUD for their property; lock-device read; vendor-adapter read |
front_desk | Issue/update/revoke guest key credentials; read lock devices |
housekeeping_lead | Master-key issue/revoke for their staff team only |
engineer | Lock-device pairing; vendor-adapter read; offline-issuance cert mint for their device |
guest (delegated via Magic Link) | None directly; receives credentials through notification-service |
Authorization is enforced both at the route layer (Casbin or simple RBAC matrix) and re-checked by aggregates (e.g., MasterKey.issue() rejects if invoking principal isn't the staff's manager).
2.4 Device binding
Electron desktops are first-party clients with device binding per ADR-0003 §Security:
- On first launch, the desktop generates an Ed25519 keypair, stores private key in OS keychain, registers public key + machine fingerprint with
iam-serviceviaPOST /api/v1/devices. A property-level admin approves the binding. - Every desktop request carries
X-Device-IdandX-Device-Sig(Ed25519 signature over canonical request body + timestamp). - Device unbinding (e.g., laptop stolen) revokes the device row in
iam-serviceand adds the device's offline-issuance cert serial to the CRL.
3. Vendor credential isolation
Vendor credentials are the highest-value asset this service custodies.
3.1 Storage
- Never in Postgres plaintext. The
vendor_credentialsrow stores only the secret resource name (projects/.../secrets/lock-vendor/{tenantId}/{vendorAdapterId}/{role}), not the secret material. - Material lives in GCP Secret Manager with:
- Per-tenant dedicated KMS key (
lock-vendor-{tenantId}inmelmastoon-kmskeyring), CMEK. - IAM grants only to this service's runtime SA:
roles/secretmanager.secretAccessorscoped per secret. - Access logged to Cloud Audit Logs; sink to BigQuery; alert on access from outside known SAs.
- Per-tenant dedicated KMS key (
3.2 Access pattern
- Fetch on demand inside the adapter call site, never at startup.
- In-memory cache ≤ 60 s, keyed by
(tenantId, vendorAdapterId, role). Invalidated onlock.vendor_credential.rotated.v1. - Never logged. Never serialized into outbox payloads. Redaction filter on logger asserts
secret_refis the only identifier present and the value matchesprojects/.../secrets/...shape; any payload containing a vendor secret pattern triggers an alert and the log is dropped.
3.3 Rotation
- Cloud Scheduler enforces per-vendor rotation policy (TTLock/Salto/Vostio: 90 days; on-prem connector mTLS: 365 days).
- Rotation flow:
RotateVendorCredentialUseCasemints new secret in Secret Manager → updatesvendor_credentialsrow pointer → emitslock.vendor_credential.rotated.v1→ in-memory caches invalidate. - Old secret version is destroyed after 7-day grace.
3.4 Compromise response
If a vendor credential is suspected leaked:
- Tenant admin (or system admin) calls
POST /api/v1/vendor-adapters/:id/credentials/revokewith reason. Sync API call — does not return until revoked at vendor (where supported) and at Secret Manager. - Service emits
lock.vendor_adapter.health_changed.v1 (status='disabled'). - All in-flight sagas using that adapter fail-fast with
MELMASTOON.LOCK.VENDOR_ADAPTER_DISABLED(503). - New credential minted via the rotation flow; saga retries pick up.
4. Offline issuance certificates
(Phase 2; for MVP launch, an offline issuance window is allowed only for tenants on the Electron desktop with a paired encoder.)
4.1 Cert lifecycle
- Cert minting:
POST /api/v1/offline-issuance— verifies device is bound, paired with an encoder, and operator hasengineerortenant_adminrole. - Cert payload:
{ tenantId, propertyId, deviceId, validFrom, validUntil, maxIssuances, allowedRooms?, allowedKinds[] }. - Cert is signed by the cloud signing key (KMS asymmetric key
lock-offline-issuance-signing/v1) — Ed25519. Public key is shipped with the desktop installer. - The desktop holds the cert + an issuance counter; each offline mint increments the counter and is included in the local outbox event.
4.2 Constraints
validUntil ≤ validFrom + 14 days(hard cap).maxIssuances ≤ 200per cert.- A given device can hold only one active cert at a time — issuing a new cert revokes the prior (added to CRL).
4.3 CRL
The CRL is published as a sync-pulled aggregate (offlineCertificates.crl). The cloud reconciler always rejects pushed events whose certSerial is in CRL with MELMASTOON.LOCK.OFFLINE_CERT_REVOKED (403); the desktop must locally revoke the affected credentials.
4.4 Forgery resistance
- Cert signature verified by the cloud reconciler on every push (cannot trust stored copy on the desktop).
- Desktop's Ed25519 device key signs each push batch; without it, the push is rejected.
- An attacker who steals the device key can forge offline credentials only until the device is unbound (which adds its cert serial to CRL); meanwhile the upper bound on damage is
maxIssuances × max_validity_window ≤ 200 × 14 daysper active cert.
5. Webhook ingress security
Vendor webhooks are an attractive spoofing target.
5.1 Per-vendor signature verification
| Vendor | Signature scheme | Header |
|---|---|---|
| TTLock | HMAC-SHA256 of body using shared secret per app | X-TTLock-Signature |
| Salto | RSA-SHA256 of body using Salto-issued public key | X-Salto-Signature |
| Vostio | mTLS client cert pinning (no header signature) + body HMAC | X-Vostio-Signature + mTLS |
| Generic Wiegand | n/a (no vendor cloud) | n/a |
The webhook handler:
- Reads raw body (not parsed JSON) to compute signature.
- Looks up the corresponding
vendor_credentials.signing_secret_refvia the tenant scoping in the URL path (/webhooks/v1/{vendor}/{vendorAdapterId}). - Verifies signature with constant-time compare.
- On failure: 401 with no body, increment
melmastoon_lock_webhook_signature_failed_total{vendor}counter; if rate ≥ 10/min from a single source IP, automatically denylist via Cloud Armor.
5.2 Replay protection
webhook_inbox.unique(vendor, vendorEventId) prevents replay; replays return 200 (idempotent) but do not re-trigger processing.
5.3 Rate limit
Per-tenant ingest rate limit: 100 webhooks/sec/vendor. Excess returns 429 (Retry-After).
6. Audit immutability
The lock_audit table is append-only at the database level:
- No
UPDATEprivilege granted to the application role (lock_app); onlyINSERTandSELECT. - No
DELETEprivilege. - A daily Cloud Run job computes a Merkle root of the prior day's
lock_auditrows (ordered byid) and submits it toaudit-service, which anchors to BigQuery + an external timestamping service per 02 §10. - The Merkle root is published as
melmastoon.lock.audit.merkle_root.v1(regulated retention, 7y). - Tampering detection: on demand, any
lock_auditrow range can be re-hashed and compared to its anchored root; mismatch → P1 incident.
key_credential_attempts rows have the same append-only protection (separate table, same controls).
7. Logging redaction
Logger middleware enforces:
vendorRef,vendor_credentials.*,OfflineIssuance.private_key, raw webhook bodies → dropped before reaching log sink.- PII fields (
guest.contact.phoneE164,guest.contact.email,guest.displayName) → masked (+93***4567,k***@example.com,K. A.). Authorizationheader → never logged.- Stack traces → keep, but scrub for vendor/secret patterns.
A periodic CI job runs a regex scan over recent log samples for known vendor secret shapes; failure → block deploy.
8. Network posture
- Cloud Run service is private (Internal + Cloud Load Balancing). Only API Gateway and Pub/Sub push subscriptions reach it.
- Webhook endpoints are exposed via a separate Cloud Run revision behind Cloud Armor with vendor IP allowlists where stable (TTLock, Salto), and mTLS for Vostio.
- Outbound to vendor APIs goes through Cloud NAT with static egress IP per region (so vendors can allowlist us).
- On-prem Salto connector terminates mTLS at a regional Cloud VPN tunnel; service traffic to it goes via private VPC.
9. Data classification (mapping)
Per 07 §6:
| Field | Class |
|---|---|
vendor_credentials.secret_ref | Secret |
KeyCredential.vendorRef | Secret-equivalent (encrypted at rest, never logged, never replicated) |
KeyCredential.guestId, staffUserId | Personal |
KeyCredentialAttempt.deviceLocation | Personal-adjacent |
LockDevice.vendor_device_ref | Confidential |
lock_audit.payload | Personal (variable) |
| Counters/health metrics | Internal |
10. Compliance hooks
- GDPR / data subject deletion:
key_credential_attempts.guestIdandkey_credentials.guestIdrows for an erased guest are pseudonymized (set togst_REDACTED_<tenantHash>) but rows are not deleted — required for retention and audit. Request handled by the platform's privacy worker vialock-integration-service.SubjectDeletionUseCase. - Data residency: Cloud SQL instance is regional per 07 §8. For EU-data tenants, dedicated regional instance is provisioned by
tenant-service; this service consults the tenant's residency tag and routes its DB connection accordingly via the regional pooler. - PCI: This service never touches payment data. Out of scope.
11. Incident playbook (summary)
| Incident | Immediate action | Reference |
|---|---|---|
| Vendor credential leaked | Revoke + rotate; emit vendor_adapter.health_changed.disabled; halt that adapter's sagas | §3.4 |
| Stolen desktop / lost device | Unbind device in iam-service; revoke offline cert (CRL); flag all credentials issued in last cert window for review | §4 |
| Master key suspected misused | Suspend master key (REST); open HITL anomaly review; alert tenant admin | AI_INTEGRATION §2.5 |
| Webhook spoofing detected | Cloud Armor denylist source; re-verify recent decisions in inbox; alert tenant | §5.1 |
| Audit Merkle mismatch | P1; halt all writes to affected partition; page security; audit-service forensic | §6 |
Full playbook in runbooks/lock-integration/ repo.