feat(security): implement ADR 0005 Authentik boundary and hosting isolation
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
---
|
||||
status: "accepted"
|
||||
date: 2026-10-11
|
||||
decision-makers: ["Moritz Graf", "Antigravity"]
|
||||
consulted: []
|
||||
informed: []
|
||||
---
|
||||
|
||||
# ADR 0005: Personal Data Isolation, Authentik Authentication Boundaries, and Hosting Segregation
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
On 2026-10-11, during the development of mobile-first PWA dashboards for baby monitoring (Mila Graf) and personal nutrition/Garmin energy balance (Moritz Graf), personal health, sleep, diaper, and nutrition data was temporarily made available at unauthenticated root URLs:
|
||||
- `https://hosting.haumdaucher.de/mila.html`
|
||||
- `https://hosting.haumdaucher.de/moritz.html`
|
||||
|
||||
While [ADR 0004](0004-structured-personal-data-datastore.md) established the conceptual routing design (`/family/*` protected by Traefik ForwardAuth, public recipes at `/recipes/*`), a gap analysis revealed critical systemic vulnerabilities:
|
||||
1. **Unapplied Kubernetes Ingress:** The ForwardAuth Ingress (`minio-hosting-family`) declared in `k8s/minio/hosting-ingress.yaml` was never applied to the Kubernetes cluster.
|
||||
2. **Missing Authentik Identity Provider:** Authentik had no Proxy Provider or Application configured for `hosting.haumdaucher.de`, returning HTTP 404 on ForwardAuth checks.
|
||||
3. **Dual-Publishing Anti-Pattern:** Dashboard generator scripts (`update_website.py`, `update_moritz_website.py`) mirrored files to root (`/mila.html`, `/moritz.html`) for convenience, completely bypassing the planned `/family/` path boundary.
|
||||
4. **Lack of Automated Ingress & Storage Auditing:** No automated test or CI/Cron audit prevented personal data from being uploaded to public prefixes.
|
||||
|
||||
How do we establish a permanent, verifiable, and automated security boundary that guarantees personal and family data can never be exposed publicly without authentication?
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
* **Zero Public Exposure for Personal Data:** Health, sleep, nutrition, body, and family data must NEVER be accessible without authentication.
|
||||
* **Defense-in-Depth:** Security must not rely on a single layer (e.g. secret URLs or developer memory). Protection must be enforced at identity provider, reverse proxy, storage bucket, and generator script levels.
|
||||
* **Declarative Infrastructure as Code (ADR 0003):** All Authentik providers, policies, groups, and Kubernetes Ingresses must be versioned in Git and applied via Terraform/Kubectl.
|
||||
* **Automated Guardrails & Fast Feedback:** Continuous audits must detect and quarantine any unauthorized files outside protected paths.
|
||||
* **Seamless PWA Experience:** The PWA capabilities (manifest, service worker, offline shell) must function within the authenticated cookie session without breaking ForwardAuth.
|
||||
|
||||
---
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
Chosen Strategy: **Multi-Layer Boundary Enforcement with ForwardAuth, Strict Storage Segregation, and Continuous Auditing**.
|
||||
|
||||
### 1. Storage & URL Path Segmentation Architecture
|
||||
|
||||
```
|
||||
Internet Request to https://hosting.haumdaucher.de
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ Traefik Ingress │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
Path Inspection & Priority
|
||||
│
|
||||
┌────────────────┴────────────────┐
|
||||
│ │
|
||||
Path: /family/* Path: /* (Fallback)
|
||||
Priority: 100 Priority: 10
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────┐ ┌─────────────────────────┐
|
||||
│ Authentik ForwardAuth │ │ Public Ingress │
|
||||
│ • Host: hosting.haumd...│ │ • ONLY /recipes/* │
|
||||
│ • Allowed: 'family' & │ │ • ONLY /showcase/* │
|
||||
│ 'infra-admins' groups │ │ • NO personal files │
|
||||
└────────────┬────────────┘ └────────────┬────────────┘
|
||||
│ │
|
||||
HTTP 200 (Authed) │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ MinIO Bucket │ │ MinIO Bucket │
|
||||
│ key: family/* │ │ key: public/* │
|
||||
│ (Protected Data) │ │ (Public Static) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
#### Rule 1: Canonical Protected Prefix `/family/`
|
||||
- All dashboards containing personal, familial, or physiological metrics MUST reside under `/family/`:
|
||||
- `https://hosting.haumdaucher.de/family/mila.html` (Baby sleep, feeds, diapers)
|
||||
- `https://hosting.haumdaucher.de/family/moritz.html` (Personal nutrition, Garmin deficits, health)
|
||||
- `https://hosting.haumdaucher.de/family/nutrition.html` (Family nutrition overview)
|
||||
- `https://hosting.haumdaucher.de/family/sleep.html` (Sleep analytics)
|
||||
- Associated PWA assets (manifests, service workers) MUST also be scoped within `/family/` (e.g. `/family/mila.webmanifest`, `/family/sw.js`).
|
||||
|
||||
#### Rule 2: Root Prefix `/` Strictly Restricted
|
||||
- The root of `hosting.haumdaucher.de` is restricted to public static assets (`/recipes/*`, `/showcase/*`).
|
||||
- Publishing scripts MUST NEVER write personal dashboards to the bucket root.
|
||||
|
||||
---
|
||||
|
||||
### 2. Declarative Authentik ForwardAuth Configuration
|
||||
|
||||
Authentik configuration is codified in `terraform/authentik.tf`:
|
||||
1. **Proxy Provider (`authentik_provider_proxy.hosting_family`):**
|
||||
- External Host: `https://hosting.haumdaucher.de`
|
||||
- Mode: `forward_single`
|
||||
- Bound to `authentik_outpost.embedded`
|
||||
- Session duration: 30 days (`access_token_validity = "days=30"`)
|
||||
2. **Application (`authentik_application.hosting_family`):**
|
||||
- Slug: `hosting-family`
|
||||
- Protocol Provider: `authentik_provider_proxy.hosting_family.id`
|
||||
3. **Role-Based Policy Bindings:**
|
||||
- Group `family` (Moritz & Amelie Graf): Granted access
|
||||
- Group `infra-admins`: Granted access
|
||||
- Unauthenticated requests are redirected (HTTP 302) to `https://auth.haumdaucher.de/application/o/authorize/...`
|
||||
|
||||
---
|
||||
|
||||
### 3. Kubernetes Ingress Deployment
|
||||
|
||||
`k8s/minio/hosting-ingress.yaml` defines the two ingress tiers:
|
||||
- **`minio-hosting-family` (Priority 100):**
|
||||
- Path: `/family` (Prefix)
|
||||
- Middlewares: `auth-authentik-forwardauth@kubernetescrd,minio-hosting-add-prefix@kubernetescrd`
|
||||
- **`minio-hosting` (Priority 10):**
|
||||
- Path: `/` (Prefix)
|
||||
- Middlewares: `minio-hosting-add-prefix@kubernetescrd`
|
||||
|
||||
---
|
||||
|
||||
### 4. Generator Scripts & Codebase Hardening
|
||||
|
||||
1. **Elimination of Root Duplication:**
|
||||
- `skills/datastore/scripts/update_website.py`: Removed all S3 upload and local sync calls to root `mila.html`. Target is strictly `family/mila.html`.
|
||||
- `skills/datastore/scripts/update_moritz_website.py`: Removed S3 upload and local sync calls to root `moritz.html`. Target is strictly `family/moritz.html` (and `family/nutrition.html`).
|
||||
2. **PWA Scope Alignment:**
|
||||
- Web App Manifests configure `start_url: "/family/mila.html"` and `scope: "/family/"`.
|
||||
- Service workers are registered at `/family/sw.js`.
|
||||
3. **Telegram Link Consistency:**
|
||||
- Agent prompts for `mila` (Topic 20) and `koch` (Topic 21) only broadcast the secure URL: `https://hosting.haumdaucher.de/family/...`.
|
||||
|
||||
---
|
||||
|
||||
### 5. Automated Continuous Leak Audit Guard
|
||||
|
||||
An automated audit check runs continuously (via OpenClaw periodic audit and datastore maintenance):
|
||||
- Inspects the MinIO `hosting` bucket root (`prefix=""`).
|
||||
- Detects any forbidden files matching patterns: `*mila*`, `*moritz*`, `*nutrition*`, `*sleep*`, `*schlaf*`, `*health*`, `*diaper*`.
|
||||
- If detected:
|
||||
1. Immediately deletes/quarantines the offending object from the public bucket root.
|
||||
2. Raises a critical notification/event to the system log.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Guaranteed Confidentiality:** Family and personal health data is physically impossible to access without passing Authentik ForwardAuth.
|
||||
- **Reproducible Infrastructure:** The entire authentication chain is codified in Terraform and Kubernetes manifests.
|
||||
- **Persistent Mobile PWA:** Authentik's 30-day session lifetime ensures seamless standalone mobile app operation without repetitive login prompts.
|
||||
- **Automated Safety Net:** Even if a developer or agent script mistakenly attempts to mirror a file to root, the automated audit deletes it.
|
||||
|
||||
### Negative & Mitigations
|
||||
- **Authentication Step on First Mobile Launch:**
|
||||
- *Mitigation*: When installing the PWA on iOS/Android, the user authenticates once via Authentik. The persistent session cookie maintains login across app launches.
|
||||
@@ -14,6 +14,7 @@ We follow the [Markdown Architectural Decision Records (MADR)](https://adr.githu
|
||||
| [**0002**](0002-openclaw-model-selection-tiering-and-economics.md) | **OpenClaw Multi-Tier Model Selection, Latency Optimization, and Inference Economics** | `proposed` | 2026-09-13 | OpenClaw / AI / Models |
|
||||
| [**0003**](0003-everything-as-code-and-reproducible-infrastructure.md) | **Everything as Code (EaC) and Reproducible Declarative Infrastructure** | `accepted` | 2026-09-17 | Architecture / Monorepo / EaC |
|
||||
| [**0004**](0004-structured-personal-data-datastore.md) | **Structured Personal Data Layer (DuckDB + JSONL in Knowledge Wiki)** | `accepted` | 2026-10-06 | OpenClaw / Data / Storage |
|
||||
| [**0005**](0005-personal-data-isolation-and-authentik-boundary.md) | **Personal Data Isolation, Authentik Authentication Boundaries, and Hosting Segregation** | `accepted` | 2026-10-11 | Security / Storage / Authentik |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
+1
-1
Submodule knowledge-wiki updated: eaf4e9ffd4...bdf2beb005
@@ -359,6 +359,23 @@ resource "authentik_application" "silverbullet" {
|
||||
protocol_provider = authentik_provider_proxy.silverbullet.id
|
||||
}
|
||||
|
||||
# 10. Hosting Family Dashboards
|
||||
resource "authentik_provider_proxy" "hosting_family" {
|
||||
name = "Hosting Family"
|
||||
external_host = "https://hosting.haumdaucher.de"
|
||||
mode = "forward_single"
|
||||
authorization_flow = data.authentik_flow.default_authorization_flow.id
|
||||
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||
intercept_header_auth = true
|
||||
access_token_validity = "days=30"
|
||||
}
|
||||
|
||||
resource "authentik_application" "hosting_family" {
|
||||
name = "Hosting Family"
|
||||
slug = "hosting-family"
|
||||
protocol_provider = authentik_provider_proxy.hosting_family.id
|
||||
}
|
||||
|
||||
# --- Outpost: Embedded Proxy Outpost ---
|
||||
|
||||
resource "authentik_outpost" "embedded" {
|
||||
@@ -374,6 +391,7 @@ resource "authentik_outpost" "embedded" {
|
||||
authentik_provider_proxy.openclaw.id,
|
||||
authentik_provider_proxy.n8n.id,
|
||||
authentik_provider_proxy.silverbullet.id,
|
||||
authentik_provider_proxy.hosting_family.id,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -759,6 +777,18 @@ resource "authentik_policy_binding" "silverbullet_family" {
|
||||
order = 1
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "hosting_family_admin" {
|
||||
target = authentik_application.hosting_family.uuid
|
||||
group = authentik_group.infra_admins.id
|
||||
order = 0
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "hosting_family_family" {
|
||||
target = authentik_application.hosting_family.uuid
|
||||
group = authentik_group.family.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
|
||||
# 2. Shared & Member Applications (Accessible to all authenticated users)
|
||||
resource "authentik_policy_binding" "grafana_users" {
|
||||
|
||||
+152
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.14.3",
|
||||
"serial": 417,
|
||||
"serial": 423,
|
||||
"lineage": "8aff5d23-05f6-10eb-0ae6-1084c787677e",
|
||||
"outputs": {
|
||||
"coder_oauth_client_secret": {
|
||||
@@ -211,7 +211,8 @@
|
||||
"date_joined": "2026-09-05T14:23:01.528947Z",
|
||||
"email": "",
|
||||
"groups": [
|
||||
"ad33b003-77a4-4571-bec8-8301f6d6552b"
|
||||
"ad33b003-77a4-4571-bec8-8301f6d6552b",
|
||||
"5e91f468-a0f2-4433-aa23-a7b6ad69df38"
|
||||
],
|
||||
"id": "7",
|
||||
"is_active": true,
|
||||
@@ -495,6 +496,41 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_application",
|
||||
"name": "hosting_family",
|
||||
"provider": "provider[\"registry.terraform.io/goauthentik/authentik\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"backchannel_providers": null,
|
||||
"group": "",
|
||||
"id": "hosting-family",
|
||||
"meta_description": "",
|
||||
"meta_hide": false,
|
||||
"meta_icon": "",
|
||||
"meta_launch_url": "",
|
||||
"meta_publisher": "",
|
||||
"name": "Hosting Family",
|
||||
"open_in_new_tab": false,
|
||||
"policy_engine_mode": "any",
|
||||
"protocol_provider": 16,
|
||||
"slug": "hosting-family",
|
||||
"uuid": "14ad2eb5-c3b8-4fe6-b980-26f1a91f44a3"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"identity_schema_version": 0,
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"authentik_provider_proxy.hosting_family",
|
||||
"data.authentik_flow.default_authorization_flow",
|
||||
"data.authentik_flow.default_invalidation_flow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_application",
|
||||
@@ -1007,8 +1043,8 @@
|
||||
"id": "5e91f468-a0f2-4433-aa23-a7b6ad69df38",
|
||||
"is_superuser": false,
|
||||
"name": "family",
|
||||
"parents": null,
|
||||
"roles": null,
|
||||
"parents": [],
|
||||
"roles": [],
|
||||
"users": [
|
||||
7
|
||||
]
|
||||
@@ -1118,7 +1154,8 @@
|
||||
7,
|
||||
3,
|
||||
10,
|
||||
15
|
||||
15,
|
||||
16
|
||||
],
|
||||
"service_connection": "",
|
||||
"type": "proxy"
|
||||
@@ -1130,6 +1167,7 @@
|
||||
"authentik_provider_proxy.alertmanager",
|
||||
"authentik_provider_proxy.gitea_phpmyadmin",
|
||||
"authentik_provider_proxy.hass_coder",
|
||||
"authentik_provider_proxy.hosting_family",
|
||||
"authentik_provider_proxy.n8n",
|
||||
"authentik_provider_proxy.nextcloud_phpmyadmin",
|
||||
"authentik_provider_proxy.openclaw",
|
||||
@@ -1393,6 +1431,73 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_policy_binding",
|
||||
"name": "hosting_family_admin",
|
||||
"provider": "provider[\"registry.terraform.io/goauthentik/authentik\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"enabled": true,
|
||||
"failure_result": false,
|
||||
"group": "c0ad76f4-d157-4c9e-882c-9ae5b5fc1690",
|
||||
"id": "55b2f74f-f650-40a2-9781-a00a5978e1e8",
|
||||
"negate": false,
|
||||
"order": 0,
|
||||
"policy": "",
|
||||
"target": "14ad2eb5-c3b8-4fe6-b980-26f1a91f44a3",
|
||||
"timeout": 30,
|
||||
"user": 0
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"identity_schema_version": 0,
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"authentik_application.hosting_family",
|
||||
"authentik_group.infra_admins",
|
||||
"authentik_provider_proxy.hosting_family",
|
||||
"data.authentik_flow.default_authorization_flow",
|
||||
"data.authentik_flow.default_invalidation_flow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_policy_binding",
|
||||
"name": "hosting_family_family",
|
||||
"provider": "provider[\"registry.terraform.io/goauthentik/authentik\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"enabled": true,
|
||||
"failure_result": false,
|
||||
"group": "5e91f468-a0f2-4433-aa23-a7b6ad69df38",
|
||||
"id": "b598a889-f04b-4955-8a01-6558cfaad60e",
|
||||
"negate": false,
|
||||
"order": 1,
|
||||
"policy": "",
|
||||
"target": "14ad2eb5-c3b8-4fe6-b980-26f1a91f44a3",
|
||||
"timeout": 30,
|
||||
"user": 0
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"identity_schema_version": 0,
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"authentik_application.hosting_family",
|
||||
"authentik_group.family",
|
||||
"authentik_provider_proxy.hosting_family",
|
||||
"data.authentik_flow.default_authorization_flow",
|
||||
"data.authentik_flow.default_invalidation_flow",
|
||||
"data.authentik_user.amy"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_policy_binding",
|
||||
@@ -2281,6 +2386,48 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_provider_proxy",
|
||||
"name": "hosting_family",
|
||||
"provider": "provider[\"registry.terraform.io/goauthentik/authentik\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"access_token_validity": "days=30",
|
||||
"authentication_flow": "",
|
||||
"authorization_flow": "68f43818-60ba-4d8e-ae66-cbac29d7be42",
|
||||
"basic_auth_enabled": false,
|
||||
"basic_auth_password_attribute": "",
|
||||
"basic_auth_username_attribute": "",
|
||||
"client_id": "27XovQEtW8nMAgUN6kANZnQZMMbWVEbi8Mh6VsGP",
|
||||
"cookie_domain": "",
|
||||
"external_host": "https://hosting.haumdaucher.de",
|
||||
"id": "16",
|
||||
"intercept_header_auth": true,
|
||||
"internal_host": "",
|
||||
"internal_host_ssl_validation": true,
|
||||
"invalidation_flow": "9add9e98-4fe7-48c5-bca8-d68968e0a7ba",
|
||||
"jwks_sources": null,
|
||||
"jwt_federation_providers": null,
|
||||
"jwt_federation_sources": null,
|
||||
"mode": "forward_single",
|
||||
"name": "Hosting Family",
|
||||
"property_mappings": null,
|
||||
"refresh_token_validity": "days=30",
|
||||
"skip_path_regex": ""
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"identity_schema_version": 0,
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"data.authentik_flow.default_authorization_flow",
|
||||
"data.authentik_flow.default_invalidation_flow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "authentik_provider_proxy",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.14.3",
|
||||
"serial": 415,
|
||||
"serial": 417,
|
||||
"lineage": "8aff5d23-05f6-10eb-0ae6-1084c787677e",
|
||||
"outputs": {
|
||||
"coder_oauth_client_secret": {
|
||||
@@ -2458,7 +2458,7 @@
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"access_token_validity": "minutes=10",
|
||||
"access_token_validity": "days=7",
|
||||
"authentication_flow": "",
|
||||
"authorization_flow": "68f43818-60ba-4d8e-ae66-cbac29d7be42",
|
||||
"basic_auth_enabled": false,
|
||||
|
||||
Reference in New Issue
Block a user