Skip to main content

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

AssetSensitivityReason
vendor_credentials.secret_ref (vendor API keys, OAuth refresh tokens, on-prem connector mTLS certs)CRITICALExfiltration → 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)CRITICALForge offline-issued credentials at any property where the cert is trusted
KeyCredential.vendorRef (encoder card serials, mobile token IDs)HIGHReplay/clone of a single guest credential
MasterKey.vendorRefCRITICALWhole-property access
lock_audit rowsHIGHTampering hides incidents from regulators and the GM
key_credential_attempts rowsMEDIUMPrivacy: who entered which room when
lock_devices.vendor_device_refMEDIUMLateral movement to vendor portal
Webhook ingress trafficHIGHSpoofed 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 carry sub, 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 with aud="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:

  1. Asserts header tenant_id ∈ token's tenant_ids[] (multi-tenant operators can hold many).
  2. Sets app.tenant_id Postgres GUC at the start of the transaction; all queries flow through RLS policies on tenant_id.
  3. On RLS violation (no rows returned from a query that should have returned at least one because tenant_id was wrong), returns MELMASTOON.IAM.TENANT_MISMATCH (403) — never silently empty results.

2.3 Role-based authorization

RoleCan call
system_adminAll endpoints (cross-tenant for support; audit-logged)
tenant_adminAll endpoints scoped to their tenant; vendor-adapter CRUD
property_managerKey credential CRUD for their property; lock-device read; vendor-adapter read
front_deskIssue/update/revoke guest key credentials; read lock devices
housekeeping_leadMaster-key issue/revoke for their staff team only
engineerLock-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-service via POST /api/v1/devices. A property-level admin approves the binding.
  • Every desktop request carries X-Device-Id and X-Device-Sig (Ed25519 signature over canonical request body + timestamp).
  • Device unbinding (e.g., laptop stolen) revokes the device row in iam-service and 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_credentials row 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} in melmastoon-kms keyring), CMEK.
    • IAM grants only to this service's runtime SA: roles/secretmanager.secretAccessor scoped per secret.
    • Access logged to Cloud Audit Logs; sink to BigQuery; alert on access from outside known SAs.

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 on lock.vendor_credential.rotated.v1.
  • Never logged. Never serialized into outbox payloads. Redaction filter on logger asserts secret_ref is the only identifier present and the value matches projects/.../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: RotateVendorCredentialUseCase mints new secret in Secret Manager → updates vendor_credentials row pointer → emits lock.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:

  1. Tenant admin (or system admin) calls POST /api/v1/vendor-adapters/:id/credentials/revoke with reason. Sync API call — does not return until revoked at vendor (where supported) and at Secret Manager.
  2. Service emits lock.vendor_adapter.health_changed.v1 (status='disabled').
  3. All in-flight sagas using that adapter fail-fast with MELMASTOON.LOCK.VENDOR_ADAPTER_DISABLED (503).
  4. 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 has engineer or tenant_admin role.
  • 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 ≤ 200 per 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 days per active cert.

5. Webhook ingress security

Vendor webhooks are an attractive spoofing target.

5.1 Per-vendor signature verification

VendorSignature schemeHeader
TTLockHMAC-SHA256 of body using shared secret per appX-TTLock-Signature
SaltoRSA-SHA256 of body using Salto-issued public keyX-Salto-Signature
VostiomTLS client cert pinning (no header signature) + body HMACX-Vostio-Signature + mTLS
Generic Wiegandn/a (no vendor cloud)n/a

The webhook handler:

  1. Reads raw body (not parsed JSON) to compute signature.
  2. Looks up the corresponding vendor_credentials.signing_secret_ref via the tenant scoping in the URL path (/webhooks/v1/{vendor}/{vendorAdapterId}).
  3. Verifies signature with constant-time compare.
  4. 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 UPDATE privilege granted to the application role (lock_app); only INSERT and SELECT.
  • No DELETE privilege.
  • A daily Cloud Run job computes a Merkle root of the prior day's lock_audit rows (ordered by id) and submits it to audit-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_audit row 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.).
  • Authorization header → 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:

FieldClass
vendor_credentials.secret_refSecret
KeyCredential.vendorRefSecret-equivalent (encrypted at rest, never logged, never replicated)
KeyCredential.guestId, staffUserIdPersonal
KeyCredentialAttempt.deviceLocationPersonal-adjacent
LockDevice.vendor_device_refConfidential
lock_audit.payloadPersonal (variable)
Counters/health metricsInternal

10. Compliance hooks

  • GDPR / data subject deletion: key_credential_attempts.guestId and key_credentials.guestId rows for an erased guest are pseudonymized (set to gst_REDACTED_<tenantHash>) but rows are not deleted — required for retention and audit. Request handled by the platform's privacy worker via lock-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)

IncidentImmediate actionReference
Vendor credential leakedRevoke + rotate; emit vendor_adapter.health_changed.disabled; halt that adapter's sagas§3.4
Stolen desktop / lost deviceUnbind device in iam-service; revoke offline cert (CRL); flag all credentials issued in last cert window for review§4
Master key suspected misusedSuspend master key (REST); open HITL anomaly review; alert tenant adminAI_INTEGRATION §2.5
Webhook spoofing detectedCloud Armor denylist source; re-verify recent decisions in inbox; alert tenant§5.1
Audit Merkle mismatchP1; halt all writes to affected partition; page security; audit-service forensic§6

Full playbook in runbooks/lock-integration/ repo.

12. Cross-references