From d7c60cf60c5b833192265f2c7e195ab3e3344345 Mon Sep 17 00:00:00 2001 From: allaun Date: Tue, 30 Jun 2026 01:49:03 -0500 Subject: [PATCH] feat(infra): secure Vaultwarden installation using Authentik API and Caddy - Set SIGNUPS_ALLOWED=false in Vaultwarden environment on cupfox to disable public signups. - Automated OIDC/Proxy setup via configure_vault_authentik.py by registering proxy provider, application, and updating the embedded outpost. - Modified /etc/caddy/Caddyfile.vault on racknerd to forward authentication requests through the Authentik outpost. - Added vaultwarden-verify-sso.spec.ts Playwright verification test ensuring SSO redirects, successful login, and disabled registration are enforced. Build: 3314 jobs, 0 errors (lake build) --- .../tests/vaultwarden-verify-sso.spec.ts | 57 +++++++++ .../shim/configure_vault_authentik.py | 121 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 4-Infrastructure/k3s-flake/tests/vaultwarden-verify-sso.spec.ts create mode 100644 4-Infrastructure/shim/configure_vault_authentik.py diff --git a/4-Infrastructure/k3s-flake/tests/vaultwarden-verify-sso.spec.ts b/4-Infrastructure/k3s-flake/tests/vaultwarden-verify-sso.spec.ts new file mode 100644 index 00000000..81601057 --- /dev/null +++ b/4-Infrastructure/k3s-flake/tests/vaultwarden-verify-sso.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; + +test('Verify Vaultwarden SSO and signup disable', async ({ page }) => { + page.on('console', msg => console.log(`Browser Console [${msg.type()}]:`, msg.text())); + page.on('requestfailed', req => console.log('Request Failed:', req.url(), req.failure()?.errorText)); + page.on('pageerror', err => console.log('Page Error:', err.message)); + + test.setTimeout(60000); + + console.log('Navigating to Vaultwarden via domain...'); + await page.goto('https://vault.researchstack.info'); + await page.waitForTimeout(5000); + + console.log('Current URL:', page.url()); + console.log('Page Title:', await page.title()); + + // 1. Verify we redirected to Authentik + expect(page.url()).toContain('auth.researchstack.info'); + expect(await page.title()).toContain('authentik'); + + // 2. Perform SSO Login + console.log('Entering SSO username...'); + await page.locator('input[placeholder*="Username"], input[name="uid"], input[type="text"]').first().fill('allaun'); + + const continueBtn = page.locator('button:has-text("Log in"), button:has-text("Continue"), input[type="submit"]').first(); + await continueBtn.click(); + await page.waitForTimeout(2000); + + console.log('Entering SSO password...'); + await page.locator('input[name="password"], input[type="password"]').first().fill('Silverkitten14'); + await continueBtn.click(); + await page.waitForTimeout(10000); + + console.log('Redirected URL after login:', page.url()); + console.log('Redirected Page Title:', await page.title()); + + // 3. Verify redirection back to Vaultwarden + expect(page.url()).toContain('vault.researchstack.info'); + expect((await page.title()).toLowerCase()).toContain('vaultwarden'); + + // 4. Verify that the 'Create account' button is NOT visible (public registration is disabled) + const createAccountBtn = page.locator('button:has-text("Create account"), a:has-text("Create account")'); + const count = await createAccountBtn.count(); + console.log('Create account button count:', count); + if (count > 0) { + const isVisible = await createAccountBtn.first().isVisible(); + console.log('Create account button isVisible:', isVisible); + expect(isVisible).toBeFalsy(); + } else { + console.log('Create account button not found in the DOM (expected).'); + } + + // 5. Take screenshot and save to artifacts + const screenshotPath = '/home/allaun/.gemini/antigravity/brain/6ba3635d-8e65-4264-89be-c9d2e259d0d3/vaultwarden-verify-final.png'; + await page.screenshot({ path: screenshotPath }); + console.log('Screenshot saved to:', screenshotPath); +}); diff --git a/4-Infrastructure/shim/configure_vault_authentik.py b/4-Infrastructure/shim/configure_vault_authentik.py new file mode 100644 index 00000000..536b1535 --- /dev/null +++ b/4-Infrastructure/shim/configure_vault_authentik.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +import os +import requests +import json +import sys + +TOKEN = os.environ.get("AUTHENTIK_TOKEN", "sLYSgzOsIO0elCJXVtpkYkTDtnkkIoGnu10CdbQxjQa6F7EO3QsRbZC3Pf0Z") +BASE_URL = "https://auth.researchstack.info" +OUTPOST_UUID = "1ceb9880-517e-4fe6-acb8-ecc1c8276bf4" + +headers = { + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "application/json", + "Accept": "application/json" +} + +def main(): + # 1. Create or get Proxy Provider + print("Checking if proxy provider 'researchstack-vault' already exists...") + resp = requests.get(f"{BASE_URL}/api/v3/providers/proxy/", headers=headers) + if resp.status_code != 200: + print(f"Error listing providers: {resp.status_code} {resp.text}") + sys.exit(1) + + providers = resp.json().get("results", []) + provider_pk = None + for p in providers: + if p.get("name") == "researchstack-vault": + provider_pk = p.get("pk") + print(f"Provider 'researchstack-vault' already exists with PK {provider_pk}") + break + + if not provider_pk: + print("Creating proxy provider...") + provider_data = { + "name": "researchstack-vault", + "authentication_flow": "b6a80fb0-d5b9-4475-ab60-e60fb3a8672d", + "authorization_flow": "70aaa90a-83bf-4506-bc63-78234f6021df", + "invalidation_flow": "b5daeb70-7587-4f74-b41b-2b5be06c5477", + "property_mappings": [ + "ad30e2ea-1754-4a29-934c-39fbf42a4045", + "4047f976-fbc0-4a75-bc8d-0537f88fc436", + "793af5d2-f445-44e5-9034-e5fe2baeed7a", + "4991aaa7-31ec-4d62-a5e1-95946aacdfdf", + "c894ca75-3c2e-4870-840b-5441bbdd65ea" + ], + "internal_host": "http://100.115.119.40:8080", + "external_host": "https://vault.researchstack.info", + "internal_host_ssl_validation": False, + "mode": "forward_single", + "intercept_header_auth": True, + "cookie_domain": "researchstack.info" + } + resp = requests.post(f"{BASE_URL}/api/v3/providers/proxy/", headers=headers, json=provider_data) + if resp.status_code not in (200, 201): + print(f"Error creating provider: {resp.status_code} {resp.text}") + sys.exit(1) + provider_pk = resp.json().get("pk") + print(f"Created proxy provider with PK {provider_pk}") + + # 2. Create or get Application + print("Checking if application 'Research Stack Vaultwarden' already exists...") + resp = requests.get(f"{BASE_URL}/api/v3/core/applications/", headers=headers) + if resp.status_code != 200: + print(f"Error listing applications: {resp.status_code} {resp.text}") + sys.exit(1) + + apps = resp.json().get("results", []) + app_exists = False + for a in apps: + if a.get("slug") == "researchstack-vault": + app_exists = True + print("Application 'Research Stack Vaultwarden' already exists") + break + + if not app_exists: + print("Creating application...") + app_data = { + "name": "Research Stack Vaultwarden", + "slug": "researchstack-vault", + "provider": provider_pk, + "policy_engine_mode": "any" + } + resp = requests.post(f"{BASE_URL}/api/v3/core/applications/", headers=headers, json=app_data) + if resp.status_code not in (200, 201): + print(f"Error creating application: {resp.status_code} {resp.text}") + sys.exit(1) + print("Created application successfully") + + # 3. Update Outpost + print("Fetching existing outpost...") + resp = requests.get(f"{BASE_URL}/api/v3/outposts/instances/{OUTPOST_UUID}/", headers=headers) + if resp.status_code != 200: + print(f"Error fetching outpost: {resp.status_code} {resp.text}") + sys.exit(1) + + outpost_data = resp.json() + providers_list = outpost_data.get("providers", []) + print(f"Current outpost providers: {providers_list}") + + if provider_pk not in providers_list: + providers_list.append(provider_pk) + print(f"Updating outpost with providers: {providers_list}") + + # We need to send a PUT or PATCH request. + # PATCH requires name, type, and providers at a minimum + update_data = { + "name": outpost_data.get("name"), + "type": outpost_data.get("type"), + "providers": providers_list + } + resp = requests.patch(f"{BASE_URL}/api/v3/outposts/instances/{OUTPOST_UUID}/", headers=headers, json=update_data) + if resp.status_code != 200: + print(f"Error updating outpost: {resp.status_code} {resp.text}") + sys.exit(1) + print("Outpost updated successfully with new provider!") + else: + print("Provider already assigned to outpost.") + +if __name__ == "__main__": + main()