Add Uptime Kuma monitoring template
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-30 11:12:15 +01:00
parent 6ca0b50403
commit 9a8305d986
7 changed files with 767 additions and 31 deletions

View File

@@ -12,7 +12,6 @@
- `SEC-FE-01` CSP nonce utility.
- Week 2
- `SEC-IO-02` refresh-token management UI. *(delivered 2025-10-23)*
- `SEC-GT-02` token analytics dashboards.
- `SEC-API-02` incident response playbook.
- `SEC-MS-02` streaming upload refactor.
- `SEC-BILL-02` webhook signature freshness.

View File

@@ -1,28 +0,0 @@
# Join Token Analytics & Alerting (SEC-GT-02)
## Data Sources
- Table `event_join_token_events` captures successes, failures, rate-limit hits, and uploads per join token.
- Each row records route, device id, IP, HTTP status, and context for post-incident drill downs.
- Logged automatically from `EventPublicController` for `/api/v1/events/*` and `/api/v1/gallery/*`.
- Super Admin: Event resource → “Join Link / QR” modal now summarises total successes/failures, rate-limit hits, 24h volume, and last activity timestamp per token.
- Tenant Admin: identical modal surface so operators can monitor invite health.
## Alert Thresholds (initial)
- **Rate limit spike**: >25 `token_rate_limited` entries for a token within 10 minutes → flag in monitoring (Grafana/Prometheus TODO).
- **Failure ratio**: failure_count / success_count > 0.5 over rolling hour triggers warning for support follow-up.
- **Inactivity**: tokens without access for >30 days should be reviewed; scheduled report TBD.
Rate-limiter knobs (see `.env.example`):
- `JOIN_TOKEN_FAILURE_LIMIT` / `JOIN_TOKEN_FAILURE_DECAY` — repeated invalid attempts before temporary block (default 10 tries per 5min).
- `JOIN_TOKEN_ACCESS_LIMIT` / `JOIN_TOKEN_ACCESS_DECAY` — successful request ceiling per token/IP (default 120 req per minute).
- `JOIN_TOKEN_DOWNLOAD_LIMIT` / `JOIN_TOKEN_DOWNLOAD_DECAY` — download ceiling per token/IP (default 60 downloads per minute).
## Follow-up Tasks
1. Wire aggregated metrics into Grafana once metrics pipeline is ready (synthetic monitors pending SEC-GT-03).
2. Implement scheduled command to email tenants a weekly digest of token activity and stale tokens.
3. Consider anonymising device identifiers before long-term retention (privacy review).
## Runbook Notes
- Analytics table may grow quickly for high-traffic events; plan nightly prune job (keep 90 days).
- Use `php artisan tinker` to inspect token activity: `EventJoinTokenEvent::where('event_join_token_id', $id)->latest()->limit(20)->get()`.

View File

@@ -17,7 +17,7 @@ The playbook focuses on abuse, availability loss, and leaked content.
| 4xx/5xx spikes | Application logs (`storage/logs/laravel.log`), centralized logging | Look for repeated `Join token access denied` / `token_rate_limited` or unexpected 5xx. |
| Rate-limit triggers | Laravel log lines emitted from `EventPublicController::handleTokenFailure` | Contains IP + truncated token preview. |
| CDN/WAF alerts | Reverse proxy (if enabled) | Ensure 429/403 anomalies are forwarded to incident channel. |
| Synthetic monitors | Planned via `SEC-API-03` | Placeholder until monitors exist. |
| Synthetic monitors | Uptime Kuma (SEC-API-03) | Public uptime + guest API + support API health checks. |
Manual check commands:
@@ -26,6 +26,32 @@ php artisan log:tail --lines=200 | grep "Join token"
php artisan log:tail --lines=200 | grep "gallery"
```
## 1.1 Synthetic Monitors (Uptime Kuma)
Primary uptime:
- `GET /` (base domain) — HTTP 200-399.
Guest API (stable synthetic event tokens required):
- `GET /api/v1/events/{token}` — expect JSON containing `"slug"` or `"engagement_mode"`.
- `GET /api/v1/gallery/{token}` — expect JSON containing `"event"` or `"branding"`.
- `GET /api/v1/gallery/{token}/photos` — expect JSON containing `"data"`.
- `GET /api/v1/events/{token}/photos` — expect JSON containing `"data"`.
Support API health metrics (read-only token stored in Kuma):
- `GET /api/v1/support/tenants?per_page=1`
- `GET /api/v1/support/events?per_page=1`
- `GET /api/v1/support/photos?per_page=1`
Defaults:
- Interval: 60s
- Timeout: 10s
- Retries before alert: 2
Notes:
- Do not store bearer tokens in the repo; configure them directly in Kuma.
- If synthetic tokens rotate, guest monitors will flap. Keep a dedicated synthetic event/token.
- Import template: `docs/ops/deployment/uptime-kuma-import.template.json` (replace placeholders before import).
## 2. Severity Classification
| Level | Criteria | Examples |

View File

@@ -0,0 +1,718 @@
{
"version": "1.23.16",
"notificationList": [],
"monitorList": [
{
"id": 1,
"name": "Uptime: Web Root",
"description": null,
"pathName": "Uptime: Web Root",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "http",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": null,
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-399"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": null,
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 2,
"name": "Guest: Event",
"description": null,
"pathName": "Guest: Event",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/events/<JOIN_TOKEN>",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "slug",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": null,
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 3,
"name": "Guest: Gallery",
"description": null,
"pathName": "Guest: Gallery",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/gallery/<GALLERY_TOKEN>",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "event",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": null,
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 4,
"name": "Guest: Gallery Photos",
"description": null,
"pathName": "Guest: Gallery Photos",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/gallery/<GALLERY_TOKEN>/photos",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "data",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": null,
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 5,
"name": "Guest: Event Photos",
"description": null,
"pathName": "Guest: Event Photos",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/events/<JOIN_TOKEN>/photos",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "data",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": null,
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 6,
"name": "Support: Tenants",
"description": null,
"pathName": "Support: Tenants",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/support/tenants?per_page=1",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "data",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": "{\"Authorization\":\"Bearer <SUPPORT_API_TOKEN>\"}",
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 7,
"name": "Support: Events",
"description": null,
"pathName": "Support: Events",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/support/events?per_page=1",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "data",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": "{\"Authorization\":\"Bearer <SUPPORT_API_TOKEN>\"}",
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
},
{
"id": 8,
"name": "Support: Photos",
"description": null,
"pathName": "Support: Photos",
"parent": null,
"childrenIDs": [],
"url": "https://fotospiel.app/api/v1/support/photos?per_page=1",
"method": "GET",
"hostname": null,
"port": null,
"maxretries": 2,
"weight": 2000,
"active": true,
"forceInactive": false,
"type": "keyword",
"timeout": 10,
"interval": 60,
"retryInterval": 60,
"resendInterval": 0,
"keyword": "data",
"invertKeyword": false,
"expiryNotification": false,
"ignoreTls": false,
"upsideDown": false,
"packetSize": 56,
"maxredirects": 10,
"accepted_statuscodes": [
"200-299"
],
"dns_resolve_type": "A",
"dns_resolve_server": "1.1.1.1",
"dns_last_result": null,
"docker_container": "",
"docker_host": null,
"proxyId": null,
"notificationIDList": {},
"tags": [],
"maintenance": false,
"mqttTopic": "",
"mqttSuccessMessage": "",
"databaseQuery": null,
"authMethod": "",
"grpcUrl": null,
"grpcProtobuf": null,
"grpcMethod": null,
"grpcServiceName": null,
"grpcEnableTls": false,
"radiusCalledStationId": null,
"radiusCallingStationId": null,
"game": null,
"gamedigGivenPortOnly": true,
"httpBodyEncoding": "json",
"jsonPath": null,
"expectedValue": null,
"kafkaProducerTopic": null,
"kafkaProducerBrokers": [],
"kafkaProducerSsl": false,
"kafkaProducerAllowAutoTopicCreation": false,
"kafkaProducerMessage": null,
"screenshot": null,
"headers": "{\"Authorization\":\"Bearer <SUPPORT_API_TOKEN>\"}",
"body": null,
"grpcBody": null,
"grpcMetadata": null,
"basic_auth_user": null,
"basic_auth_pass": null,
"oauth_client_id": null,
"oauth_client_secret": null,
"oauth_token_url": null,
"oauth_scopes": null,
"oauth_auth_method": "client_secret_basic",
"pushToken": null,
"databaseConnectionString": null,
"radiusUsername": null,
"radiusPassword": null,
"radiusSecret": null,
"mqttUsername": "",
"mqttPassword": "",
"authWorkstation": null,
"authDomain": null,
"tlsCa": null,
"tlsCert": null,
"tlsKey": null,
"kafkaProducerSaslOptions": {
"mechanism": "None"
},
"includeSensitiveData": false
}
]
}