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)
This commit is contained in:
allaun 2026-06-30 01:49:03 -05:00
parent 8c09c239c8
commit d7c60cf60c
2 changed files with 178 additions and 0 deletions

View file

@ -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);
});

View file

@ -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()