170 lines
11 KiB
Markdown
170 lines
11 KiB
Markdown
# Configuration reference
|
|
|
|
The notification delivery platform binds under `ca-skeleton.notification.platform`. The tree lives in
|
|
`src/app-bootstrap/src/main/resources/application.yml`, disabled by default, and every value carries
|
|
an inline default so a deployment that leaves the platform off supplies nothing.
|
|
|
|
Until 2026-08-15 this page named properties the binding did not have — `max-retry-concurrency`,
|
|
`scheduler-poll-interval`, `callback-worker-concurrency` — and omitted three it did. There was no
|
|
tree in `application.yml` at all, so the only way to configure the platform was to guess environment
|
|
variable names from Boot's relaxed binding. `./gradlew verifyNotificationConfiguration` now fails
|
|
when this page, the YAML tree and `docs/registries/env-keys.yaml` disagree.
|
|
|
|
## Master switch
|
|
|
|
| Property | Environment variable | Default | Meaning |
|
|
|---|---|---|---|
|
|
| `enabled` | `APP_NOTIFICATION_PLATFORM_ENABLED` | `false` | Binds nothing at all while false: no runtime, no schema check, no scheduler thread, no secret required |
|
|
| `mode` | `APP_NOTIFICATION_PLATFORM_MODE` | `SERVING` | `SERVING` refuses to start without a working provider; `INGEST_ONLY` stores requests and does not dispatch |
|
|
|
|
## Dispatch
|
|
|
|
| Property | Environment variable | Default | Bound |
|
|
|---|---|---|---|
|
|
| `dispatch.claim-batch-size` | `APP_NOTIFICATION_PLATFORM_CLAIM_BATCH_SIZE` | `50` | 1..1000 |
|
|
| `dispatch.lease-duration` | `APP_NOTIFICATION_PLATFORM_LEASE_DURATION` | `2m` | positive, finite |
|
|
| `dispatch.poll-interval` | `APP_NOTIFICATION_PLATFORM_POLL_INTERVAL` | `1s` | positive, finite |
|
|
| `dispatch.max-global-concurrency` | `APP_NOTIFICATION_PLATFORM_MAX_CONCURRENCY` | `64` | positive |
|
|
| `dispatch.max-additional-attempts` | `APP_NOTIFICATION_PLATFORM_MAX_ADDITIONAL_ATTEMPTS` | `4` | non-negative |
|
|
| `dispatch.max-queue-age` | `APP_NOTIFICATION_PLATFORM_MAX_QUEUE_AGE` | `24h` | positive |
|
|
| `dispatch.allow-ambiguous-fallback` | `APP_NOTIFICATION_PLATFORM_ALLOW_AMBIGUOUS_FALLBACK` | `false` | boolean |
|
|
|
|
Every value is bounded. "Unlimited" is not an accepted configuration.
|
|
|
|
The lease must outlast a provider call plus its timeout. Below that, a delivery a live worker is
|
|
still waiting on gets claimed by a second worker, and the recipient receives the notification twice.
|
|
|
|
## Callbacks
|
|
|
|
| Property | Environment variable | Default | Bound |
|
|
|---|---|---|---|
|
|
| `callbacks.enabled` | `APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED` | `false` | boolean |
|
|
| `callbacks.max-body-bytes` | `APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES` | `65508` | 1..65508 |
|
|
| `callbacks.replay-skew` | `APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW` | `5m` | positive |
|
|
| `callbacks.trusted-proxies` | `APP_NOTIFICATION_PLATFORM_CALLBACK_TRUSTED_PROXIES` | *(empty)* | CSV of peer addresses |
|
|
|
|
여러 provider가 요청 URL에 서명하므로, 그 URL을 잘못 재구성하면 정상 webhook이 전부 서명 실패가 된다.
|
|
`trusted-proxies`가 비어 있으면 forwarded 헤더를 **믿지 않고** 컨테이너가 관측한 값을 쓴다. 무조건 믿으면
|
|
아무 호출자나 자기 서명이 검증될 URL을 고를 수 있어 서명 자체가 무의미해진다. 로드밸런서 뒤에 있는 배포는
|
|
그 peer를 명시한다.
|
|
|
|
65508 is not a round number by accident: it is the ciphertext column's 65536 bytes minus the AES-GCM
|
|
nonce and tag. A larger configured value would pass every check above the database and fail the
|
|
`CHECK` constraint after the callback had already been acknowledged to the provider.
|
|
|
|
## Provider profiles
|
|
|
|
Profiles are a map under `providers`, keyed by profile id. There are no environment variables for
|
|
them, because the keys are deployment-chosen; supply them as YAML or as
|
|
`CA_SKELETON_NOTIFICATION_PLATFORM_PROVIDERS_<ID>_<FIELD>`.
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `type` | `APNS`, `FCM`, `SES`, `SMTP`, `TWILIO`, `WEB_PUSH`, `WEBHOOK` — a closed enum, so an unknown value fails binding rather than assembling into nothing |
|
|
| `enabled` | A disabled profile is bound and validated but contributes no runtime |
|
|
| `primary-for-channel` | Exactly one primary per channel |
|
|
| `environment` | Required when enabled |
|
|
| `credential-profile` | Resolved through `SecretMaterialProvider`; never an inline secret |
|
|
| `topic` | APNs bundle id |
|
|
| `vapid-public-key` | Web Push application server key |
|
|
| `callback-signing-secret-ref` | Reference, not material |
|
|
| `timeout` | Positive and finite |
|
|
| `max-concurrency` | Positive |
|
|
| `rate-per-second` | Positive |
|
|
|
|
A profile pins provider type, environment, credential profile, timeouts, concurrency and rate limit.
|
|
Sender identity and credential profile are separate concerns.
|
|
|
|
## SMTP relay
|
|
|
|
The one provider profile the template ships, off. A deployment that wants the common case sets
|
|
`APP_NOTIFICATION_PLATFORM_SMTP_ENABLED=true` and the relay address; one that wants a different
|
|
profile id or a second family declares it in its own YAML instead.
|
|
|
|
The profile and the relay are separate tables below because they answer different questions. The
|
|
profile says *which* provider serves EMAIL and under what limits; the relay says *what the transport
|
|
is*. Host, port and credentials are not here at all — they stay `spring.mail.*`, because Spring
|
|
already owns them and a second spelling would be a second thing to keep in step.
|
|
|
|
| Property | Environment variable | Default | Bound |
|
|
|---|---|---|---|
|
|
| `providers.smtp.enabled` | `APP_NOTIFICATION_PLATFORM_SMTP_ENABLED` | `false` | boolean |
|
|
| `providers.smtp.primary-for-channel` | `APP_NOTIFICATION_PLATFORM_SMTP_PRIMARY` | `true` | boolean; exactly one primary per channel |
|
|
| `providers.smtp.environment` | `APP_NOTIFICATION_PLATFORM_SMTP_ENVIRONMENT` | `local` | required when enabled |
|
|
| `providers.smtp.credential-profile` | `APP_NOTIFICATION_PLATFORM_SMTP_CREDENTIAL_PROFILE` | `default` | resolved through `SecretMaterialProvider`, never inline material |
|
|
| `providers.smtp.timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_TIMEOUT` | `10s` | positive, finite |
|
|
| `providers.smtp.max-concurrency` | `APP_NOTIFICATION_PLATFORM_SMTP_MAX_CONCURRENCY` | `4` | positive |
|
|
| `providers.smtp.rate-per-second` | `APP_NOTIFICATION_PLATFORM_SMTP_RATE_PER_SECOND` | `10` | positive |
|
|
|
|
| Property | Environment variable | Default | Bound |
|
|
|---|---|---|---|
|
|
| `smtp.tls-mode` | `APP_NOTIFICATION_PLATFORM_SMTP_TLS_MODE` | `STARTTLS_REQUIRED` | `STARTTLS_REQUIRED` or `IMPLICIT_TLS` |
|
|
| `smtp.sender-identity` | `APP_NOTIFICATION_PLATFORM_SMTP_SENDER_IDENTITY` | `no-reply@example.invalid` | address |
|
|
| `smtp.connect-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_CONNECT_TIMEOUT` | `5s` | positive, finite |
|
|
| `smtp.read-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_READ_TIMEOUT` | `10s` | positive, finite |
|
|
| `smtp.write-timeout` | `APP_NOTIFICATION_PLATFORM_SMTP_WRITE_TIMEOUT` | `10s` | positive, finite |
|
|
| `smtp.max-concurrency` | `APP_NOTIFICATION_PLATFORM_SMTP_DISPATCH_CONCURRENCY` | `4` | positive |
|
|
|
|
The TLS mode enum has no plaintext member. An unencrypted relay is refused by construction rather
|
|
than by a validator somebody has to remember to run.
|
|
|
|
The default sender is an RFC 2606 reserved domain that resolves nowhere, so a deployment that forgot
|
|
to set one produces a traceable bounce instead of mail apparently sent from an address it does not
|
|
own.
|
|
|
|
## Startup failures
|
|
|
|
Startup fails rather than degrading when:
|
|
|
|
- a payload or queue setting is unbounded
|
|
- a timeout is negative
|
|
- a TTL-required profile has no expiry source
|
|
- a callback signing secret is missing
|
|
- a production profile enables trust-all
|
|
- an APNs profile is missing its environment or topic
|
|
- a Web Push profile is missing its VAPID key
|
|
- two provider profiles share an id
|
|
- a route points only at disabled providers
|
|
- `mode` is `SERVING` and no provider profile is enabled
|
|
- the notification schema stream is not applied and promoted
|
|
|
|
## Secrets
|
|
|
|
All key material arrives through `SecretMaterialProvider`. Nothing is read from source, from a
|
|
committed file, or from a plaintext log. Contact point encryption and lookup HMAC keys must be
|
|
distinct, and the encryption key must be exactly 256 bits.
|
|
|
|
Eight purposes, eight keys. Each is base64 of at least 32 bytes and each must differ from every
|
|
other; the platform decodes them at startup and refuses to boot if one is blank, short or shared. A
|
|
blank value used to be skipped, which meant the platform started without the key and found out on
|
|
the first contact point — in production, on a recipient's notification.
|
|
|
|
Every default below is **unset**, deliberately. Supply the values out of band, per environment. Do
|
|
not write one into this table, into `application.yml`, into an `.env` file that is tracked, or into
|
|
any example: a value that appears in the repository is a value that has been disclosed.
|
|
|
|
| Purpose | Key material | Active key id |
|
|
|---|---|---|
|
|
| Contact point encryption | `APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY` | `APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY_ID` |
|
|
| Contact point lookup HMAC | `APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY_ID` |
|
|
| Callback signing | `APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY` | `APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY_ID` |
|
|
| Callback fingerprint HMAC | `APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY_ID` |
|
|
| Provider credential encryption | `APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY` | `APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY_ID` |
|
|
| Provider request lookup HMAC | `APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY` | `APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY_ID` |
|
|
| Payload encryption | `APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY` | `APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY_ID` |
|
|
| Web Push VAPID signing | `APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY` | `APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY_ID` |
|
|
|
|
A key id is not secret — an id identifies key material without revealing it — but it is required,
|
|
and it has no default on purpose. A constant id makes a rotation indistinguishable from the key it
|
|
replaced, so nothing could decrypt what was written before it. Change the id in the same deployment
|
|
that changes the material, and keep the superseded key readable under its old id until the data it
|
|
wrote has been re-encrypted. The rotation sequence is in
|
|
[at-rest-threat-model.md](at-rest-threat-model.md).
|
|
|
|
## Readiness
|
|
|
|
The platform contributes a `notifications` actuator endpoint and a health indicator. It reports DOWN
|
|
when a provider's credentials were rejected, when a configured provider has no channel route, and
|
|
when the measured backlog, stuck-lease count, projection lag or reconciliation lag passes the
|
|
thresholds in `NotificationServingThresholds`. See [operations.md](operations.md).
|