Appearance
ADR: Local encrypted secret store + SecretRef
Status
Accepted
Date
2026-06-25
Context
MyHoreca Pay stores Syrve/iiko connection credentials for organization-scoped integrations. These credentials are security-critical: a leaked password, API key, or token can allow unauthorized upstream access and unsafe payment writes.
The product already accepts that:
- PostgreSQL is the source of truth for permanent configuration, business records, and audit history.
- Redis stores only temporary runtime state with TTL, including short-lived Syrve/iiko runtime tokens.
- Permanent credentials, passwords, API keys, refresh secrets, and connection secrets are forbidden in Redis.
- MVP requires credentials rotation in UI for authorized administrators.
Without a shared secret-storage ADR, future backend, frontend, and integration tasks can accidentally store raw credentials on SyrveConnection, return secrets from API, place permanent secrets in Redis, or grant rotation rights to operational roles that must not manage credentials.
This ADR is documentation-only. It must not introduce runtime backend or frontend code changes, EF migrations, permission enum/seeds/policies changes, real credentials, real bank files, or private customer data.
Decision
For MVP, MyHoreca Pay adopts Local encrypted secret store + SecretRef.
Decisions:
SyrveConnectionstores only aSecretRef, not raw login/password/token/API key values.SecretRecordstores the encrypted credential payload in approved local encrypted secret storage backed by PostgreSQL.- Raw password/token/API key/credential value is never returned by public API.
- Runtime Syrve/iiko token is stored only in Redis as TTL-bound runtime state according to the accepted Redis runtime policy.
- Permanent credentials, passwords, API keys, refresh secrets, and connection secrets are forbidden in Redis.
- Credentials rotation UI is required in MVP.
- Rotation UI may change
login,password,token, or other allowed credential value fields. - After creation,
server address/base URLis immutable connection identity. If a different server/base URL is needed, create a new connection. - Rotation UI is available to
MegaAdmin,MegaManager, andOrganizationAdmin. syrve.credentials.rotateis not granted toAccountantorViewer.PaymentOperatorandLocationAdminare not MVP roles and must not become mandatory dependencies for credentials rotation.
SecretRef
SecretRef is a stable reference stored on SyrveConnection.
Rules:
SecretRefdoes not reveal secret value.SecretRefincludes enough identity to locate the active encryptedSecretRecordor active secret version later.SecretRefis safe to return across internal service boundaries only when it does not expose encrypted value or raw secret.- Public API may expose connection metadata and masked status, but must not expose
SecretRefif doing so would help reconstruct or retrieve secret material outside authorized resolver paths. Future implementation may expose non-sensitive reference metadata only when explicitly approved by API design.
Documentation shape for SecretRef:
json
{
"secretId": "uuid-v7",
"organizationId": "uuid-v7",
"connectionId": "uuid-v7",
"purpose": "syrve.connection.credentials",
"activeVersion": 3
}The schema is documentation-only. Future runtime implementation must use typed DTOs and the project JSON policy.
SecretRecord
SecretRecord stores encrypted credential payload for a tenant-scoped secret.
Rules:
SecretRecordis tenant-scoped byorganizationIdand optionally by outlet/connection scope where needed.SecretRecordmust not store plaintext in database or logs.- Encrypted secret value must not be returned by public API.
- Old versions may be retained/disabled according to future implementation policy, but raw values must remain unavailable through API, logs, audit, Redis, or frontend storage.
- Secret resolver/decrypt operations are backend-internal only and must fail closed on scope mismatch or decrypt failure.
Documentation metadata fields:
| Field | Meaning |
|---|---|
secretId | Stable secret identity. |
organizationId | Owning organization scope. |
connectionId | Associated SyrveConnection identity. |
purpose | Secret purpose, for example syrve.connection.credentials. |
version | Monotonic secret version. |
createdAtUtc | First version creation timestamp. |
rotatedAtUtc | Last rotation timestamp for this version. |
createdByUserId | Actor who created the version. |
rotatedByUserId | Actor who rotated to this version. |
status | active, disabled, or future lifecycle states. |
correlationId | Trace id for rotation/reconnect/audit correlation. |
Encrypted payload is documentation-only and must use project-approved encryption at rest in future implementation. Example credential payload fields allowed inside encrypted blob:
loginpasswordtoken- other upstream-required credential fields explicitly approved for Syrve/iiko connection auth
Do not store plaintext equivalents outside the encrypted payload.
Connection identity rules
SyrveConnection immutable identity after creation:
server addressbase URL
Mutable only through credentials rotation workflow:
loginpasswordtoken- other allowed credential value fields
If the product needs a different server/base URL, operators must create a new connection. Rotation endpoints and UI must reject attempts to mutate immutable connection identity fields.
Rotation workflow
Authorized credentials rotation workflow:
- Authorized user opens connection credentials rotation UI for a scoped
SyrveConnection. - UI never pre-fills existing secret value.
- UI may submit new
login,password,token, or other allowed credential values. - Backend checks tenant scope and requires
syrve.credentials.rotate. - Backend writes a new encrypted
SecretRecordor new secret version. SyrveConnection.SecretRefpoints to the active secret/version.- Backend invalidates/deletes Redis runtime token for the connection using key pattern
mhpay:{env}:org:{organizationId}:syrve:runtime-token:{connectionId}. - Backend emits audit/security event without raw secret values.
- Reconnect/probe uses new credentials through the secret resolver.
- Failed rotation must fail closed and must not expose old or new secret values in API responses, logs, validation errors, audit, telemetry, or frontend error surfaces.
Rotation audit/security event metadata may include:
- actor user id
- organization id
- optional outlet id
- connection id
- secret id
- new version number
- action such as
syrve.credentials.rotated - timestamp
- correlation id
- result status
Rotation audit must not include raw or encrypted credential values.
API behavior
Public and operator API rules:
- Public read endpoints may expose connection metadata and masked status only.
- Raw secret values are never returned.
- Encrypted secret values are never returned.
server address/base URLcannot be updated after creation.- Rotation endpoint accepts only allowed credential fields and never accepts unrelated connection identity mutation.
- Logs, validation errors, audit events, exceptions, telemetry, and frontend error surfaces must not include raw secret values.
- Connection create endpoint may accept initial credential values once, persist them only through encrypted
SecretRecord, and storeSecretRefonSyrveConnection. - Connection read endpoints may expose whether credentials exist, last rotation metadata, and auth health status, but not secret material.
Future implementation should treat any accidental secret echo as a security defect.
Permission mapping
Target permission:
syrve.credentials.rotate
| Role | MVP decision |
|---|---|
MegaAdmin | Allow |
MegaManager | Allow |
OrganizationAdmin | Allow within owned organization and inherited child organizations |
Accountant | Deny |
Viewer | Deny |
PaymentOperator | Not MVP / Deny |
LocationAdmin | Not MVP / Deny |
This permission mapping is target documentation for MVP. It is not current runtime behavior until a future implementation task updates ScopedPermissionNames, role seeds, authorization policies, frontend permission guards, API docs, and tests.
Current runtime constants do not yet include syrve.credentials.rotate. Do not edit enums/seeds/policies in this ADR-only task.
Follow-up implementation must:
- add
syrve.credentials.rotateor an explicitly mapped equivalent constant; - grant it only to
MegaAdmin,MegaManager, andOrganizationAdminwithin scope; - deny it for
Accountant,Viewer,PaymentOperator, andLocationAdmin; - add RBAC tests for allow/deny and cross-tenant isolation.
Related target permission from the permission matrix: organization.credentials.manage may be used as a broader future constant, but rotation must remain explicitly gated by syrve.credentials.rotate or a deliberate alias documented in implementation.
403 reconnect behavior
On Syrve/iiko 403 or auth failure for a connection:
- Invalidate/delete Redis runtime token for that connection.
- Attempt reconnect/login using permanent credentials resolved via
SecretRef. - Perform at most 7 reconnect/login attempts for the failure episode.
- Attempts must be bounded, auditable, and correlated with
correlationId. - After 7 failed attempts, mark connection/auth state as requiring credentials rotation or admin attention.
- Emit event/SLA signal for operations/support.
- Do not log or expose raw credential values.
- Fail closed if credentials cannot be resolved or decrypt fails.
Reconnect audit/event metadata may include:
- organization id
- connection id
- attempt number
- upstream status/code category
- action such as
syrve.auth.reconnect_attemptorsyrve.auth.reconnect_exhausted - timestamp
- correlation id
It must not include password, token, API key, encrypted secret blob, or Redis runtime token payload.
Redis boundary
Accepted Redis runtime token policy:
- Key:
mhpay:{env}:org:{organizationId}:syrve:runtime-token:{connectionId} - Allowed: short-lived runtime access token/session state, upstream expiry metadata, connection id, correlation id
- Forbidden: permanent credentials, API keys, passwords, refresh secrets, connection secrets
On rotation or auth failure handling, backend must delete/invalidate the runtime token key before reconnecting with permanent credentials from encrypted secret storage.
Secret leakage prohibitions
The following are forbidden:
- raw credentials in API responses;
- raw credentials in logs;
- raw credentials in audit;
- raw credentials in Redis;
- raw credentials in frontend local/session storage;
- raw credentials in docs/examples/tests;
- secrets in Redis key names;
- real customer/bank files or real payment/credential data in repository.
Encrypted secret values are also forbidden in public API responses even though they are not plaintext.
Non-goals
- Runtime backend changes in this task.
- Frontend changes in this task.
- EF migrations in this task.
- Permission enum/seeds/policies changes in this task.
- External secret manager adoption beyond local encrypted secret store for MVP.
- Adding
PaymentOperatororLocationAdminas MVP rotation roles. - Committing real secrets, credentials, bank files, or private customer data.
How AI agents must use this ADR
- Read this ADR before tasks touching Syrve credentials, secret storage, connection rotation, Redis runtime tokens, reconnect/auth retry, RBAC permissions for credentials, or secret-related audit.
- If a future task conflicts with this ADR, propose a new ADR or an update to this ADR first.
- Do not implement runtime code from this ADR-only task.
- Preserve
SecretRefonSyrveConnectionand encryptedSecretRecordseparation. - Never return raw or encrypted credential values from API.
- Never store permanent credentials in Redis.
- Keep server/base URL immutable after connection creation.
- Keep rotation limited to
MegaAdmin,MegaManager, andOrganizationAdminviasyrve.credentials.rotatein future implementation.
Consequences
- Future backend secret store implementation has a stable MVP model: local encrypted storage,
SecretRef, versionedSecretRecord, rotation workflow, and reconnect policy. - Syrve connection UI/API can expose rotation without ever echoing stored secrets.
- Redis remains runtime-token-only for Syrve/iiko auth state.
- Permission work remains a separate runtime follow-up task.
- Security review can use this ADR to reject secret leakage in logs, audit, Redis, and API contracts.