diff --git a/Scripts/authentik-sync-tailscale-oidc.sh b/Scripts/authentik-sync-tailscale-oidc.sh index 58fe7e4..54564ad 100755 --- a/Scripts/authentik-sync-tailscale-oidc.sh +++ b/Scripts/authentik-sync-tailscale-oidc.sh @@ -10,8 +10,6 @@ template_slug="${AUTHENTIK_TAILSCALE_TEMPLATE_SLUG:-ts}" client_id="${AUTHENTIK_TAILSCALE_CLIENT_ID:-tailscale.burrow.net}" client_secret="${AUTHENTIK_TAILSCALE_CLIENT_SECRET:-}" launch_url="${AUTHENTIK_TAILSCALE_LAUNCH_URL:-https://login.tailscale.com/start/oidc}" -access_group="${AUTHENTIK_TAILSCALE_ACCESS_GROUP:-}" -default_external_application_slug="${AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG:-}" redirect_uris_json="${AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON:-[ \"https://login.tailscale.com/a/oauth_response\" ]}" @@ -33,8 +31,6 @@ Optional environment: AUTHENTIK_TAILSCALE_CLIENT_ID AUTHENTIK_TAILSCALE_LAUNCH_URL AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON - AUTHENTIK_TAILSCALE_ACCESS_GROUP - AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG EOF } @@ -127,111 +123,6 @@ wait_for_authentik() { wait_for_authentik -lookup_group_pk() { - local group_name="$1" - - api GET "/api/v3/core/groups/?page_size=200" \ - | jq -r --arg group_name "$group_name" '.results[]? | select(.name == $group_name) | .pk // empty' \ - | head -n1 -} - -lookup_application_pk() { - local slug="$1" - local application_pk lookup_result lookup_status - - application_pk="$( - api GET "/api/v3/core/applications/?page_size=200" \ - | jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \ - | head -n1 - )" - - if [[ -n "$application_pk" ]]; then - printf '%s\n' "$application_pk" - return 0 - fi - - lookup_result="$(api_with_status GET "/api/v3/core/applications/${slug}/")" - lookup_status="$(printf '%s\n' "$lookup_result" | sed -n '1p')" - if [[ "$lookup_status" =~ ^20[01]$ ]]; then - printf '%s\n' "$lookup_result" | sed '1d' | jq -r '.pk // empty' - fi -} - -ensure_application_group_binding() { - local application_slug="$1" - local group_name="$2" - local application_pk group_pk existing payload binding_pk - - application_pk="$(lookup_application_pk "$application_slug")" - if [[ -z "$application_pk" ]]; then - echo "warning: could not resolve Authentik application ${application_slug}; skipping application group binding" >&2 - return 0 - fi - - group_pk="$(lookup_group_pk "$group_name")" - if [[ -z "$group_pk" ]]; then - echo "error: could not resolve Authentik group ${group_name}" >&2 - exit 1 - fi - - existing="$( - api GET "/api/v3/policies/bindings/?page_size=200&target=${application_pk}" \ - | jq -c --arg group_pk "$group_pk" '.results[]? | select(.group == $group_pk)' \ - | head -n1 - )" - - payload="$( - jq -cn \ - --arg target "$application_pk" \ - --arg group "$group_pk" \ - '{ - group: $group, - target: $target, - negate: false, - enabled: true, - order: 100, - timeout: 30, - failure_result: false - }' - )" - - if [[ -n "$existing" ]]; then - binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" - api PATCH "/api/v3/policies/bindings/${binding_pk}/" "$payload" >/dev/null - else - api POST "/api/v3/policies/bindings/" "$payload" >/dev/null - fi -} - -ensure_default_external_application() { - local application_slug="$1" - local application_pk default_brand brand_payload - - application_pk="$(lookup_application_pk "$application_slug")" - if [[ -z "$application_pk" ]]; then - echo "error: could not resolve Authentik application ${application_slug} for brand default application" >&2 - exit 1 - fi - - default_brand="$( - api GET "/api/v3/core/brands/?page_size=200" \ - | jq -c '.results[]? | select(.default == true)' \ - | head -n1 - )" - - if [[ -z "$default_brand" ]]; then - echo "warning: could not resolve the default Authentik brand; skipping external default application" >&2 - return 0 - fi - - brand_payload="$( - printf '%s\n' "$default_brand" \ - | jq --arg application_pk "$application_pk" '.default_application = $application_pk' - )" - - api PUT "/api/v3/core/brands/$(printf '%s\n' "$default_brand" | jq -r '.brand_uuid')/" "$brand_payload" >/dev/null -} - template_provider="$( api GET "/api/v3/providers/oauth2/?page_size=200" \ | jq -c --arg template_slug "$template_slug" '.results[]? | select(.assigned_application_slug == $template_slug)' \ @@ -322,7 +213,6 @@ existing_application="$( if [[ -n "$existing_application" ]]; then application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')" - api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null else create_application_result="$( api_with_status POST "/api/v3/core/applications/" "$application_payload" @@ -349,14 +239,6 @@ if [[ -z "${application_pk:-}" ]]; then exit 1 fi -if [[ -n "$access_group" ]]; then - ensure_application_group_binding "$application_slug" "$access_group" -fi - -if [[ -n "$default_external_application_slug" ]]; then - ensure_default_external_application "$default_external_application_slug" -fi - for _ in $(seq 1 30); do if curl -fsS "${authentik_url}/application/o/${application_slug}/.well-known/openid-configuration" >/dev/null 2>&1; then echo "Synced Authentik Tailscale OIDC application ${application_slug} (${application_name})." diff --git a/Scripts/authentik-sync-zulip-saml.sh b/Scripts/authentik-sync-zulip-saml.sh deleted file mode 100644 index 6767991..0000000 --- a/Scripts/authentik-sync-zulip-saml.sh +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}" -bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}" -application_slug="${AUTHENTIK_ZULIP_APPLICATION_SLUG:-zulip}" -application_name="${AUTHENTIK_ZULIP_APPLICATION_NAME:-Zulip}" -provider_name="${AUTHENTIK_ZULIP_PROVIDER_NAME:-Zulip}" -acs_url="${AUTHENTIK_ZULIP_ACS_URL:-https://chat.burrow.net/complete/saml/}" -audience="${AUTHENTIK_ZULIP_AUDIENCE:-https://chat.burrow.net}" -launch_url="${AUTHENTIK_ZULIP_LAUNCH_URL:-https://chat.burrow.net/}" -access_group="${AUTHENTIK_ZULIP_ACCESS_GROUP:-}" -issuer="${AUTHENTIK_ZULIP_ISSUER:-$authentik_url}" - -usage() { - cat <<'EOF' -Usage: Scripts/authentik-sync-zulip-saml.sh - -Required environment: - AUTHENTIK_BOOTSTRAP_TOKEN - -Optional environment: - AUTHENTIK_URL - AUTHENTIK_ZULIP_APPLICATION_SLUG - AUTHENTIK_ZULIP_APPLICATION_NAME - AUTHENTIK_ZULIP_PROVIDER_NAME - AUTHENTIK_ZULIP_ACS_URL - AUTHENTIK_ZULIP_AUDIENCE - AUTHENTIK_ZULIP_LAUNCH_URL - AUTHENTIK_ZULIP_ACCESS_GROUP - AUTHENTIK_ZULIP_ISSUER -EOF -} - -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -if [[ -z "$bootstrap_token" ]]; then - echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2 - exit 1 -fi - -api() { - local method="$1" - local path="$2" - local data="${3:-}" - - if [[ -n "$data" ]]; then - curl -fsS \ - -X "$method" \ - -H "Authorization: Bearer ${bootstrap_token}" \ - -H "Content-Type: application/json" \ - -d "$data" \ - "${authentik_url}${path}" - else - curl -fsS \ - -X "$method" \ - -H "Authorization: Bearer ${bootstrap_token}" \ - "${authentik_url}${path}" - fi -} - -api_with_status() { - local method="$1" - local path="$2" - local data="${3:-}" - local response_file status - - response_file="$(mktemp)" - trap 'rm -f "$response_file"' RETURN - - if [[ -n "$data" ]]; then - status="$( - curl -sS \ - -o "$response_file" \ - -w '%{http_code}' \ - -X "$method" \ - -H "Authorization: Bearer ${bootstrap_token}" \ - -H "Content-Type: application/json" \ - -d "$data" \ - "${authentik_url}${path}" - )" - else - status="$( - curl -sS \ - -o "$response_file" \ - -w '%{http_code}' \ - -X "$method" \ - -H "Authorization: Bearer ${bootstrap_token}" \ - "${authentik_url}${path}" - )" - fi - - printf '%s\n' "$status" - cat "$response_file" -} - -wait_for_authentik() { - for _ in $(seq 1 90); do - if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then - return 0 - fi - sleep 2 - done - - echo "error: Authentik did not become ready at ${authentik_url}" >&2 - exit 1 -} - -lookup_oauth_template_field() { - local field="$1" - - api GET "/api/v3/providers/oauth2/?page_size=200" \ - | jq -r --arg field "$field" '.results[]? | select(.assigned_application_slug == "ts") | .[$field]' \ - | head -n1 -} - -lookup_group_pk() { - local group_name="$1" - - api GET "/api/v3/core/groups/?page_size=200" \ - | jq -r --arg group_name "$group_name" '.results[]? | select(.name == $group_name) | .pk // empty' \ - | head -n1 -} - -lookup_application_pk() { - local slug="$1" - - api GET "/api/v3/core/applications/?page_size=200" \ - | jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \ - | head -n1 -} - -ensure_application_group_binding() { - local application_slug="$1" - local group_name="$2" - local application_pk group_pk existing payload binding_pk - - application_pk="$(lookup_application_pk "$application_slug")" - if [[ -z "$application_pk" ]]; then - echo "warning: could not resolve Authentik application ${application_slug}; skipping application group binding" >&2 - return 0 - fi - - group_pk="$(lookup_group_pk "$group_name")" - if [[ -z "$group_pk" ]]; then - echo "error: could not resolve Authentik group ${group_name}" >&2 - exit 1 - fi - - existing="$( - api GET "/api/v3/policies/bindings/?page_size=200&target=${application_pk}" \ - | jq -c --arg group_pk "$group_pk" '.results[]? | select(.group == $group_pk)' \ - | head -n1 - )" - - payload="$( - jq -cn \ - --arg target "$application_pk" \ - --arg group "$group_pk" \ - '{ - group: $group, - target: $target, - negate: false, - enabled: true, - order: 100, - timeout: 30, - failure_result: false - }' - )" - - if [[ -n "$existing" ]]; then - binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')" - api PATCH "/api/v3/policies/bindings/${binding_pk}/" "$payload" >/dev/null - else - api POST "/api/v3/policies/bindings/" "$payload" >/dev/null - fi -} - -reconcile_property_mapping() { - local name="$1" - local saml_name="$2" - local friendly_name="$3" - local expression="$4" - local payload existing_pk - - payload="$( - jq -n \ - --arg name "$name" \ - --arg saml_name "$saml_name" \ - --arg friendly_name "$friendly_name" \ - --arg expression "$expression" \ - '{ - name: $name, - saml_name: $saml_name, - friendly_name: $friendly_name, - expression: $expression - }' - )" - - existing_pk="$( - api GET "/api/v3/propertymappings/provider/saml/?page_size=200" \ - | jq -r --arg name "$name" '.results[]? | select(.name == $name) | .pk' \ - | head -n1 - )" - - if [[ -n "$existing_pk" ]]; then - api PATCH "/api/v3/propertymappings/provider/saml/${existing_pk}/" "$payload" >/dev/null - printf '%s\n' "$existing_pk" - else - api POST "/api/v3/propertymappings/provider/saml/" "$payload" | jq -r '.pk // empty' - fi -} - -wait_for_authentik - -authorization_flow="$(lookup_oauth_template_field authorization_flow)" -invalidation_flow="$(lookup_oauth_template_field invalidation_flow)" -signing_kp="$(lookup_oauth_template_field signing_key)" - -if [[ -z "$authorization_flow" || -z "$invalidation_flow" || -z "$signing_kp" ]]; then - echo "error: could not resolve Authentik provider defaults from Burrow Tailnet template" >&2 - exit 1 -fi - -email_mapping_pk="$( - reconcile_property_mapping \ - "Burrow Zulip SAML Email" \ - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" \ - "email" \ - 'return request.user.email' -)" - -name_mapping_pk="$( - reconcile_property_mapping \ - "Burrow Zulip SAML Name" \ - "name" \ - "name" \ - 'return request.user.name or request.user.username' -)" - -first_name_mapping_pk="$( - reconcile_property_mapping \ - "Burrow Zulip SAML First Name" \ - "firstName" \ - "firstName" \ - $'parts = (request.user.name or "").split(" ", 1)\nif len(parts) > 0 and parts[0]:\n return parts[0]\nreturn request.user.username' -)" - -last_name_mapping_pk="$( - reconcile_property_mapping \ - "Burrow Zulip SAML Last Name" \ - "lastName" \ - "lastName" \ - $'parts = (request.user.name or "").rsplit(" ", 1)\nif len(parts) == 2 and parts[1]:\n return parts[1]\nreturn request.user.username' -)" - -if [[ -z "$email_mapping_pk" || -z "$name_mapping_pk" || -z "$first_name_mapping_pk" || -z "$last_name_mapping_pk" ]]; then - echo "error: failed to reconcile Zulip SAML property mappings" >&2 - exit 1 -fi - -provider_payload="$( - jq -n \ - --arg name "$provider_name" \ - --arg authorization_flow "$authorization_flow" \ - --arg invalidation_flow "$invalidation_flow" \ - --arg acs_url "$acs_url" \ - --arg audience "$audience" \ - --arg issuer "$issuer" \ - --arg signing_kp "$signing_kp" \ - --arg name_id_mapping "$email_mapping_pk" \ - --arg email_mapping "$email_mapping_pk" \ - --arg name_mapping "$name_mapping_pk" \ - --arg first_name_mapping "$first_name_mapping_pk" \ - --arg last_name_mapping "$last_name_mapping_pk" \ - '{ - name: $name, - authorization_flow: $authorization_flow, - invalidation_flow: $invalidation_flow, - acs_url: $acs_url, - audience: $audience, - issuer: $issuer, - signing_kp: $signing_kp, - sign_assertion: true, - sign_response: true, - sp_binding: "post", - name_id_mapping: $name_id_mapping, - property_mappings: [ - $email_mapping, - $name_mapping, - $first_name_mapping, - $last_name_mapping - ] - }' -)" - -existing_provider="$( - api GET "/api/v3/providers/saml/?page_size=200" \ - | jq -c \ - --arg application_slug "$application_slug" \ - --arg provider_name "$provider_name" \ - '.results[]? | select(.assigned_application_slug == $application_slug or .name == $provider_name)' \ - | head -n1 -)" - -if [[ -n "$existing_provider" ]]; then - provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')" - api PATCH "/api/v3/providers/saml/${provider_pk}/" "$provider_payload" >/dev/null -else - provider_pk="$( - api POST "/api/v3/providers/saml/" "$provider_payload" \ - | jq -r '.pk // empty' - )" -fi - -if [[ -z "${provider_pk:-}" ]]; then - echo "error: Zulip SAML provider did not return a primary key" >&2 - exit 1 -fi - -application_payload="$( - jq -n \ - --arg name "$application_name" \ - --arg slug "$application_slug" \ - --arg provider "$provider_pk" \ - --arg launch_url "$launch_url" \ - '{ - name: $name, - slug: $slug, - provider: ($provider | tonumber), - meta_launch_url: $launch_url, - open_in_new_tab: true, - policy_engine_mode: "any" - }' -)" - -existing_application="$( - api GET "/api/v3/core/applications/?page_size=200" \ - | jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \ - | head -n1 -)" - -if [[ -n "$existing_application" ]]; then - application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')" - api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null -else - create_application_result="$( - api_with_status POST "/api/v3/core/applications/" "$application_payload" - )" - create_application_status="$(printf '%s\n' "$create_application_result" | sed -n '1p')" - create_application_body="$(printf '%s\n' "$create_application_result" | sed '1d')" - - if [[ "$create_application_status" =~ ^20[01]$ ]]; then - application_pk="$(printf '%s\n' "$create_application_body" | jq -r '.pk // empty')" - elif [[ "$create_application_status" == "400" ]] && printf '%s\n' "$create_application_body" | jq -e ' - (.slug // [] | index("Application with this slug already exists.")) != null - or (.provider // [] | index("Application with this provider already exists.")) != null - ' >/dev/null; then - application_pk="existing-duplicate" - else - printf '%s\n' "$create_application_body" >&2 - echo "error: could not reconcile Authentik application ${application_slug}" >&2 - exit 1 - fi -fi - -if [[ -z "${application_pk:-}" ]]; then - echo "error: Zulip SAML application did not return a primary key" >&2 - exit 1 -fi - -if [[ -n "$access_group" ]]; then - ensure_application_group_binding "$application_slug" "$access_group" -fi - -for _ in $(seq 1 30); do - metadata_status="$( - curl -sS \ - -o /dev/null \ - -w '%{http_code}' \ - --max-redirs 0 \ - "${authentik_url}/application/saml/${application_slug}/metadata/" \ - || true - )" - case "$metadata_status" in - 200|301|302|307|308) - echo "Synced Authentik Zulip SAML application ${application_slug} (${application_name})." - exit 0 - ;; - esac - sleep 2 -done - -echo "warning: Zulip SAML metadata for ${application_slug} was not immediately readable; keeping reconciled config." >&2 -echo "Synced Authentik Zulip SAML application ${application_slug} (${application_name})." diff --git a/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md b/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md index 0ce03a6..63e0994 100644 --- a/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md +++ b/evolution/proposals/BEP-0008-authentik-backed-team-chat-and-workspace-sso.md @@ -49,10 +49,6 @@ across vendor-native Google auth flows when Burrow already operates an IdP. - Add a Burrow-managed Zulip workload on the forge host at `chat.burrow.net`. The deployment should be repo-owned and rebuildable from Nix, even if the runtime uses vendor-supported container images internally. -- Prefer host-managed NixOS services for Zulip's stateful dependencies - (PostgreSQL, Redis, RabbitMQ, memcached, backups) so Burrow owns the - operational surface directly rather than composing a container-side service - mesh. - Zulip should authenticate through Authentik SAML rather than local passwords as the primary path. Initial bootstrap may still keep an operational escape hatch while the deployment is being validated. @@ -72,9 +68,6 @@ across vendor-native Google auth flows when Burrow already operates an IdP. options instead of hand-edited UI state. - Prefer service-specific reconciliation over ad hoc manual setup so rebuilds and host replacement converge automatically. -- When Burrow wants an external-user launcher surface in Authentik, configure - the brand's `default_application` explicitly instead of relying on - `/if/user/`, which otherwise remains internal-user-only. - Derive Linear SCIM role groups from Burrow's canonical identity metadata. If Burrow-wide admin intent says a user is an operator/admin, the repo-owned configuration should map that intent onto the Linear push group without a @@ -118,10 +111,8 @@ across vendor-native Google auth flows when Burrow already operates an IdP. - Verify: - `chat.burrow.net` serves a working Zulip login surface - Authentik exposes working metadata for Zulip and Linear -- Authentik exposes a working OIDC issuer for 1Password + - Authentik exposes a working OIDC issuer for 1Password - users in Burrow admin groups receive the expected access on first login - - external Burrow users landing on `auth.burrow.net` reach the intended - app launcher target instead of the internal-only Authentik user interface - Record concrete evidence for: - host deployment generation - Authentik reconciliation success diff --git a/flake.nix b/flake.nix index e842fba..1974f17 100644 --- a/flake.nix +++ b/flake.nix @@ -214,7 +214,6 @@ nixosModules.burrow-forgejo-nsc = nsc-autoscaler.nixosModules.default; nixosModules.burrow-authentik = import ./nixos/modules/burrow-authentik.nix; nixosModules.burrow-headscale = import ./nixos/modules/burrow-headscale.nix; - nixosModules.burrow-zulip = import ./nixos/modules/burrow-zulip.nix; nixosConfigurations.burrow-forge = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index c4fc92e..0121f92 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -61,7 +61,6 @@ in self.nixosModules.burrow-forgejo-nsc self.nixosModules.burrow-authentik self.nixosModules.burrow-headscale - self.nixosModules.burrow-zulip ]; system.stateVersion = "24.11"; @@ -163,37 +162,9 @@ in mode = "0400"; }; - age.secrets.burrowZulipPostgresPassword = { - file = ../../../secrets/infra/zulip-postgres-password.age; - owner = "root"; - group = "root"; - mode = "0400"; - }; - - age.secrets.burrowZulipRabbitmqPassword = { - file = ../../../secrets/infra/zulip-rabbitmq-password.age; - owner = "root"; - group = "root"; - mode = "0400"; - }; - - age.secrets.burrowZulipRedisPassword = { - file = ../../../secrets/infra/zulip-redis-password.age; - owner = "root"; - group = "root"; - mode = "0400"; - }; - - age.secrets.burrowZulipSecretKey = { - file = ../../../secrets/infra/zulip-secret-key.age; - owner = "root"; - group = "root"; - mode = "0400"; - }; - networking.extraHosts = '' - 127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net - ::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net + 127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net + ::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net ''; services.burrow.forge = { @@ -237,14 +208,12 @@ in forgejoClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path; headscaleClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path; tailscaleClientSecretFile = config.age.secrets.burrowTailscaleOidcClientSecret.path; - defaultExternalApplicationSlug = "tailscale"; googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path; googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path; googleAccountMapFile = config.age.secrets.burrowAuthentikGoogleAccountMap.path; googleLoginMode = "redirect"; userGroupName = contributors.groups.users; adminGroupName = contributors.groups.admins; - tailscaleAccessGroupName = contributors.groups.users; bootstrapUsers = bootstrapUsers; linearAcsUrl = "https://api.linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de/acs"; linearAudience = "https://auth.linear.app/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de"; @@ -255,7 +224,6 @@ in linearOwnerGroupName = linearGroups.owners; linearAdminGroupName = linearGroups.admins; linearGuestGroupName = linearGroups.guests; - zulipAccessGroupName = contributors.groups.users; }; services.burrow.headscale = { @@ -263,13 +231,4 @@ in oidcClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path; bootstrapUsers = headscaleBootstrapUsers; }; - - services.burrow.zulip = { - enable = true; - administratorEmail = identities.contact.canonicalEmail; - postgresPasswordFile = config.age.secrets.burrowZulipPostgresPassword.path; - rabbitmqPasswordFile = config.age.secrets.burrowZulipRabbitmqPassword.path; - redisPasswordFile = config.age.secrets.burrowZulipRedisPassword.path; - secretKeyFile = config.age.secrets.burrowZulipSecretKey.path; - }; } diff --git a/nixos/modules/burrow-authentik.nix b/nixos/modules/burrow-authentik.nix index acf76ce..772adc4 100644 --- a/nixos/modules/burrow-authentik.nix +++ b/nixos/modules/burrow-authentik.nix @@ -12,7 +12,6 @@ let forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh; tailscaleOidcSyncScript = ../../Scripts/authentik-sync-tailscale-oidc.sh; onePasswordOidcSyncScript = ../../Scripts/authentik-sync-1password-oidc.sh; - zulipSamlSyncScript = ../../Scripts/authentik-sync-zulip-saml.sh; linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh; linearScimSyncScript = ../../Scripts/authentik-sync-linear-scim.sh; googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh; @@ -154,18 +153,6 @@ in description = "Host-local file containing the Authentik Tailscale OIDC client secret."; }; - tailscaleAccessGroupName = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Authentik group that should be allowed to launch the Tailscale application."; - }; - - defaultExternalApplicationSlug = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Authentik application slug that external users should land on instead of /if/user/."; - }; - onePasswordDomain = lib.mkOption { type = lib.types.str; default = "burrow-team.1password.com"; @@ -199,42 +186,6 @@ in description = "Authentik application slug for Linear SAML."; }; - zulipDomain = lib.mkOption { - type = lib.types.str; - default = "chat.burrow.net"; - description = "Public Zulip domain exposed through Authentik SAML."; - }; - - zulipProviderSlug = lib.mkOption { - type = lib.types.str; - default = "zulip"; - description = "Authentik application slug for Zulip SAML."; - }; - - zulipAcsUrl = lib.mkOption { - type = lib.types.str; - default = "https://${config.services.burrow.authentik.zulipDomain}/complete/saml/"; - description = "Zulip SAML ACS URL."; - }; - - zulipAudience = lib.mkOption { - type = lib.types.str; - default = "https://${config.services.burrow.authentik.zulipDomain}"; - description = "Zulip SAML audience/entity identifier."; - }; - - zulipLaunchUrl = lib.mkOption { - type = lib.types.str; - default = "https://${config.services.burrow.authentik.zulipDomain}/"; - description = "Zulip URL exposed in Authentik."; - }; - - zulipAccessGroupName = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Authentik group allowed to launch Zulip from Burrow SSO surfaces."; - }; - linearAcsUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; @@ -858,12 +809,6 @@ EOF export AUTHENTIK_TAILSCALE_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.tailscaleClientSecretFile})" export AUTHENTIK_TAILSCALE_LAUNCH_URL=https://login.tailscale.com/start/oidc export AUTHENTIK_TAILSCALE_REDIRECT_URIS_JSON='["https://login.tailscale.com/a/oauth_response"]' - ${lib.optionalString (cfg.tailscaleAccessGroupName != null) '' - export AUTHENTIK_TAILSCALE_ACCESS_GROUP=${lib.escapeShellArg cfg.tailscaleAccessGroupName} - ''} - ${lib.optionalString (cfg.defaultExternalApplicationSlug != null) '' - export AUTHENTIK_DEFAULT_EXTERNAL_APPLICATION_SLUG=${lib.escapeShellArg cfg.defaultExternalApplicationSlug} - ''} ${pkgs.bash}/bin/bash ${tailscaleOidcSyncScript} ''; @@ -914,53 +859,6 @@ EOF ''; }; - systemd.services.burrow-authentik-zulip-saml = { - description = "Reconcile the Burrow Authentik Zulip SAML application"; - after = [ - "burrow-authentik-ready.service" - "network-online.target" - ]; - wants = [ - "burrow-authentik-ready.service" - "network-online.target" - ]; - wantedBy = [ "multi-user.target" ]; - restartTriggers = [ - zulipSamlSyncScript - cfg.envFile - ]; - path = [ - pkgs.bash - pkgs.coreutils - pkgs.curl - pkgs.jq - ]; - serviceConfig = { - Type = "oneshot"; - User = "root"; - Group = "root"; - }; - script = '' - set -euo pipefail - set -a - source ${lib.escapeShellArg cfg.envFile} - set +a - - export AUTHENTIK_URL=https://${cfg.domain} - export AUTHENTIK_ZULIP_APPLICATION_SLUG=${lib.escapeShellArg cfg.zulipProviderSlug} - export AUTHENTIK_ZULIP_APPLICATION_NAME=Zulip - export AUTHENTIK_ZULIP_PROVIDER_NAME=Zulip - export AUTHENTIK_ZULIP_ACS_URL=${lib.escapeShellArg cfg.zulipAcsUrl} - export AUTHENTIK_ZULIP_AUDIENCE=${lib.escapeShellArg cfg.zulipAudience} - export AUTHENTIK_ZULIP_LAUNCH_URL=${lib.escapeShellArg cfg.zulipLaunchUrl} - ${lib.optionalString (cfg.zulipAccessGroupName != null) '' - export AUTHENTIK_ZULIP_ACCESS_GROUP=${lib.escapeShellArg cfg.zulipAccessGroupName} - ''} - - ${pkgs.bash}/bin/bash ${zulipSamlSyncScript} - ''; - }; - systemd.services.burrow-authentik-linear-saml = lib.mkIf ( cfg.linearAcsUrl != null && cfg.linearAudience != null ) { diff --git a/nixos/modules/burrow-zulip.nix b/nixos/modules/burrow-zulip.nix deleted file mode 100644 index 0096b65..0000000 --- a/nixos/modules/burrow-zulip.nix +++ /dev/null @@ -1,486 +0,0 @@ -{ config, lib, pkgs, ... }: - -let - cfg = config.services.burrow.zulip; - yamlFormat = pkgs.formats.yaml { }; - composeFile = yamlFormat.generate "burrow-zulip-compose.yaml" { - services = { - zulip = { - image = "ghcr.io/zulip/zulip-server:11.6-1"; - restart = "unless-stopped"; - network_mode = "host"; - secrets = [ - "zulip__postgres_password" - "zulip__rabbitmq_password" - "zulip__redis_password" - "zulip__secret_key" - "zulip__email_password" - ]; - environment = { - SETTING_REMOTE_POSTGRES_HOST = "127.0.0.1"; - SETTING_MEMCACHED_LOCATION = "127.0.0.1:11211"; - SETTING_RABBITMQ_HOST = "127.0.0.1"; - SETTING_REDIS_HOST = "127.0.0.1"; - }; - volumes = [ "${cfg.dataDir}/data:/data:rw" ]; - ulimits.nofile = { - soft = 1000000; - hard = 1048576; - }; - }; - }; - }; -in -{ - options.services.burrow.zulip = { - enable = lib.mkEnableOption "the Burrow Zulip deployment"; - - domain = lib.mkOption { - type = lib.types.str; - default = "chat.burrow.net"; - description = "Public Zulip domain."; - }; - - port = lib.mkOption { - type = lib.types.port; - default = 18090; - description = "Local loopback port Caddy should proxy to."; - }; - - dataDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/burrow/zulip"; - description = "Host directory storing Zulip compose state and generated runtime files."; - }; - - administratorEmail = lib.mkOption { - type = lib.types.str; - default = "contact@burrow.net"; - description = "Operational Zulip administrator email."; - }; - - realmName = lib.mkOption { - type = lib.types.str; - default = "Burrow"; - description = "Initial Zulip organization name for single-tenant bootstrap."; - }; - - realmOwnerName = lib.mkOption { - type = lib.types.str; - default = "Burrow"; - description = "Display name used for the initial Zulip organization owner."; - }; - - authentikDomain = lib.mkOption { - type = lib.types.str; - default = config.services.burrow.authentik.domain; - description = "Authentik domain Zulip should trust as its SAML IdP."; - }; - - authentikProviderSlug = lib.mkOption { - type = lib.types.str; - default = config.services.burrow.authentik.zulipProviderSlug; - description = "Authentik SAML application slug used for Zulip."; - }; - - postgresPasswordFile = lib.mkOption { - type = lib.types.str; - description = "File containing the Zulip PostgreSQL password."; - }; - - rabbitmqPasswordFile = lib.mkOption { - type = lib.types.str; - description = "File containing the Zulip RabbitMQ password."; - }; - - redisPasswordFile = lib.mkOption { - type = lib.types.str; - description = "File containing the Zulip Redis password."; - }; - - secretKeyFile = lib.mkOption { - type = lib.types.str; - description = "File containing the Zulip Django secret key."; - }; - }; - - config = lib.mkIf cfg.enable { - environment.systemPackages = [ - pkgs.podman - pkgs.podman-compose - ]; - - services.postgresql = { - ensureDatabases = [ "zulip" ]; - ensureUsers = [ - { - name = "zulip"; - ensureDBOwnership = true; - } - ]; - settings = { - listen_addresses = lib.mkDefault "127.0.0.1"; - password_encryption = lib.mkDefault "scram-sha-256"; - }; - authentication = lib.mkAfter '' - host zulip zulip 127.0.0.1/32 scram-sha-256 - ''; - }; - - services.postgresqlBackup = { - enable = true; - backupAll = false; - databases = [ "zulip" ]; - }; - - services.memcached = { - enable = true; - listen = "127.0.0.1"; - port = 11211; - extraOptions = [ "-U 0" ]; - }; - - services.redis.servers.zulip = { - enable = true; - bind = "127.0.0.1"; - port = 6379; - requirePassFile = cfg.redisPasswordFile; - }; - - services.rabbitmq = { - enable = true; - listenAddress = "127.0.0.1"; - port = 5672; - }; - - services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' - encode gzip zstd - reverse_proxy 127.0.0.1:${toString cfg.port} - ''; - - systemd.tmpfiles.rules = [ - "d ${cfg.dataDir} 0755 root root - -" - "d ${cfg.dataDir}/data 0755 root root - -" - "d ${cfg.dataDir}/data/logs 0755 root root - -" - "d ${cfg.dataDir}/data/logs/emails 0755 root root - -" - "d ${cfg.dataDir}/data/secrets 0700 root root - -" - "d ${cfg.dataDir}/secrets 0700 root root - -" - "d ${cfg.dataDir}/logs 0755 root root - -" - ]; - - systemd.services.burrow-zulip-postgres-bootstrap = { - description = "Bootstrap PostgreSQL role for Burrow Zulip"; - after = [ "postgresql.service" ]; - wants = [ "postgresql.service" ]; - requiredBy = [ "burrow-zulip.service" ]; - before = [ "burrow-zulip.service" ]; - path = [ - config.services.postgresql.package - pkgs.bash - pkgs.coreutils - pkgs.python3 - pkgs.util-linux - ]; - serviceConfig = { - Type = "oneshot"; - User = "root"; - Group = "root"; - }; - script = '' - set -euo pipefail - - db_password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.postgresPasswordFile})" - db_password_sql="$(printf '%s' "$db_password" | python3 -c "import sys; print(sys.stdin.read().replace(chr(39), chr(39) * 2), end=\"\")")" - setup_sql="$(mktemp)" - trap 'rm -f "$setup_sql"' EXIT - - cat > "$setup_sql" < ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"} - chmod 0600 ${lib.escapeShellArg "${cfg.dataDir}/secrets/email-password"} - - metadata_xml="$(${pkgs.curl}/bin/curl -fsSL https://${cfg.authentikDomain}/application/saml/${cfg.authentikProviderSlug}/metadata/)" - saml_cert="$(printf '%s' "$metadata_xml" | ${pkgs.python3}/bin/python3 -c ' -import xml.etree.ElementTree as ET, sys -xml = sys.stdin.read() -root = ET.fromstring(xml) -ns = {"ds": "http://www.w3.org/2000/09/xmldsig#"} -node = root.find(".//ds:X509Certificate", ns) -if node is None or not (node.text or "").strip(): - raise SystemExit("missing X509 certificate in Authentik metadata") -print((node.text or "").strip()) -')" - - cat > ${lib.escapeShellArg "${cfg.dataDir}/compose.override.yaml"} < "$zulip_data_dir/secrets/bootstrap-owner-password" - fi - chown 1000:1000 "$zulip_data_dir/secrets/bootstrap-owner-password" - chmod 0600 "$zulip_data_dir/secrets/bootstrap-owner-password" - } - - bootstrap_realm_if_needed() { - local realm_exists - local attempts=0 - while ! podman exec burrow-zulip_zulip_1 test -r /etc/zulip/zulip-secrets.conf >/dev/null 2>&1; do - attempts=$((attempts + 1)) - if [ "$attempts" -ge 90 ]; then - echo "error: Zulip did not finish generating production secrets" >&2 - exit 1 - fi - sleep 2 - done - - realm_exists="$( - podman exec burrow-zulip_zulip_1 bash -lc \ - "su zulip -c '/home/zulip/deployments/current/manage.py list_realms'" \ - | awk '$NF == "https://${cfg.domain}" { print "yes" }' - )" - - if [ -n "$realm_exists" ]; then - return 0 - fi - - local realm_name=${lib.escapeShellArg cfg.realmName} - local admin_email=${lib.escapeShellArg cfg.administratorEmail} - local owner_name=${lib.escapeShellArg cfg.realmOwnerName} - local create_realm_cmd - - printf -v create_realm_cmd '%q ' \ - /home/zulip/deployments/current/manage.py \ - create_realm \ - --string-id= \ - --password-file /data/secrets/bootstrap-owner-password \ - --automated \ - "$realm_name" \ - "$admin_email" \ - "$owner_name" - - podman exec burrow-zulip_zulip_1 su zulip -c "$create_realm_cmd" - } - - if [ ! -e .initialized ]; then - compose pull - compose run --rm -T zulip app:init - touch .initialized - fi - - ensure_zulip_data_layout - compose up -d zulip - bootstrap_realm_if_needed - ''; - }; - }; -} diff --git a/secrets.nix b/secrets.nix index 3f9bba4..1a6dce0 100644 --- a/secrets.nix +++ b/secrets.nix @@ -25,9 +25,4 @@ in "secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients; "secrets/infra/linear-scim-token.age".publicKeys = burrowForgeRecipients; "secrets/infra/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients; - "secrets/infra/zulip-postgres-password.age".publicKeys = burrowForgeRecipients; - "secrets/infra/zulip-memcached-password.age".publicKeys = burrowForgeRecipients; - "secrets/infra/zulip-rabbitmq-password.age".publicKeys = burrowForgeRecipients; - "secrets/infra/zulip-redis-password.age".publicKeys = burrowForgeRecipients; - "secrets/infra/zulip-secret-key.age".publicKeys = burrowForgeRecipients; } diff --git a/secrets/infra/zulip-memcached-password.age b/secrets/infra/zulip-memcached-password.age deleted file mode 100644 index 0769512..0000000 --- a/secrets/infra/zulip-memcached-password.age +++ /dev/null @@ -1,11 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 ux4N8Q x0r1UHgSibFIvKU34kP0+mnvQa5xXnac3P5fyqb7qFc -MfKnr5N0DV2NIoo4MFVFV0ULMayy0zzZqIq4FDzgDGc --> ssh-ed25519 IrZmAg rzoR8knGrsTGuh9Hqg/NB0NQKI1vx1WI0ZRyrLIPwVY -7gV/d1slrIT+W0+iX5YK/uUWjHGJfee6vA+f9a35nEY --> ssh-ed25519 0kWPgQ SyuEAfqmBAqLcuuQUHM5OzAv2hoquMMYtVdbKpBVhjI -7QqXens2363ln0euoormMh9a3Csh+nS2eBkHuQJmOWc --> X25519 qDjNNkYBUhWTYyBhrw9tYl8a7G6TCkVZbR4aPcP+J0c -QF33V6hFUuYRj0B8Eo4jqyyvCpBbpD2ViVWoS8A8f3E ---- 1/Jb0nvWlcszMmxI0yVr6kfexDN0sSk1p+wsTUL4WvU -9a5IكV[f,Db \v&LZ7!?4=JxFeV \ No newline at end of file diff --git a/secrets/infra/zulip-postgres-password.age b/secrets/infra/zulip-postgres-password.age deleted file mode 100644 index b03556c..0000000 Binary files a/secrets/infra/zulip-postgres-password.age and /dev/null differ diff --git a/secrets/infra/zulip-rabbitmq-password.age b/secrets/infra/zulip-rabbitmq-password.age deleted file mode 100644 index 9b1f6ec..0000000 --- a/secrets/infra/zulip-rabbitmq-password.age +++ /dev/null @@ -1,11 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 ux4N8Q s1hLIWvkXmlIv/VeHXpDSCe+dh09mE+iZd7xJiQccy0 -8WosTJQLGRPhTR06SIDjgtXNebcf+H/pFzY/lBCjXcs --> ssh-ed25519 IrZmAg zBNlK+o/RCTCyp8BRkoAYqsDn//kIKtYk3SICkMu3BA -EhBQy8QdSnCZKkdGzQho7zEMmAbJVoU5jZOMPN6tHG0 --> ssh-ed25519 0kWPgQ hv06idPXqAATkLeUC5vILdEO2NXNWPczlWnwMFvOdkA -3EeajviunGlcfcF1QlRJrVA9bwPT+fJZFX0uneYVs0c --> X25519 vm9rPYnQB16VSidi7+nr70lFaH0W/jIGY8zwUObZUV8 -jFgPy/w4j0/p1USKGjQY+coo1OUFXiIjJ5apIZCrZVI ---- Cf2c6WzLYOi8xE/sIn7ZtUqBy5AToASDUNpAxyjrI9M -:,+!ϨϬB4DmH|(9l9LPZ^zed=imz? \ No newline at end of file diff --git a/secrets/infra/zulip-redis-password.age b/secrets/infra/zulip-redis-password.age deleted file mode 100644 index 2aff8b6..0000000 --- a/secrets/infra/zulip-redis-password.age +++ /dev/null @@ -1,11 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 ux4N8Q DqDE3ZZlPUWUyyLA185xsOmfGi146SNk+hENMQXaiFY -D6FhZgynbdccPJQiFRJ18EYvCyDLz3cak0YuQa4f5p4 --> ssh-ed25519 IrZmAg lXgVeADmgjeHeVOOIS5oHqrhkN59ZWDemMOBJo3ubH8 -AQ24P+DnxNoHEguNnLaROIW4/Sq96w/UxzzQwEOyGRc --> ssh-ed25519 0kWPgQ 8x0pMohdACYueLY6jbNwg7MYVaZcjwBU4axthvDoFx4 -SgUVnd6MK1MccWVYOu9R3PtoMCBBNGKQ7jt5MSA+KkI --> X25519 UaO5huJPx8d8eMUnGhbI77tZjsFlIPWEffT4fgoO22w -DVz016ibRxJoa4TDmb2m0Qu9Dn8jpjWEBVtdm2TZx0c ---- 5+MHuvC26SjEBFSmRm0kXjiI27QnJGxvPl2w13EkMrw -FoQ]ȟeU//no.XGJ Э|+ž \ No newline at end of file diff --git a/secrets/infra/zulip-secret-key.age b/secrets/infra/zulip-secret-key.age deleted file mode 100644 index d903d66..0000000 --- a/secrets/infra/zulip-secret-key.age +++ /dev/null @@ -1,11 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 ux4N8Q ml+kmLmuRb2nMXJyhKigby2+lPddxM/U7tjhGGQ/JGk -B3UCv/3+4GHeKR964o/m0CoicHwDgWQGEarPW94tb3I --> ssh-ed25519 IrZmAg AO0ELOuGGj+WanDZFRkHKUEJyZqJYFdhWbqmUfwbpiM -5RZMxVBvW5+TzCBFnn66ry3o5V5cJykweyoYMVBgczY --> ssh-ed25519 0kWPgQ gqQ/S33Re2OYLz1D9LoSAoqOKxuL4aUes8r6+NyAoXw -NHo2xFsxxJO1ZjnG9r3oxMuvjOUsCyyPvcar2ejZp9w --> X25519 vUAjBCE197YsckVNM4SYVIPBEESTWnBPCWnUlEwYs1I -L3l85DXFoAVm2ssHfjBeqRpWGlo1UGbmcNkEgoUB9fM ---- X/2O8ufjbTGrt2zCm4gSRqqoxT5v6a+13XjH4dpRsHs -Mkf"(qxF2BdMRYji ܴ<ґb_.!r+<Ussu?gD\V am(Ȉ&.& c/|w(WH4rѠ+j"B  \ No newline at end of file