Compare commits
No commits in common. "2af7618f5265471f4048db49eb1353924cf322f6" and "7d3e7a6ec56e1739526235a05d2c16857fc4cdfa" have entirely different histories.
2af7618f52
...
7d3e7a6ec5
13 changed files with 3 additions and 1207 deletions
|
|
@ -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})."
|
||||
|
|
|
|||
|
|
@ -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})."
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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" <<SQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'zulip') THEN
|
||||
CREATE ROLE zulip LOGIN;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
ALTER ROLE zulip WITH LOGIN PASSWORD '$db_password_sql';
|
||||
SQL
|
||||
chmod 0644 "$setup_sql"
|
||||
|
||||
${pkgs.util-linux}/bin/runuser -u postgres -- psql -v ON_ERROR_STOP=1 -f "$setup_sql"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.burrow-zulip-rabbitmq-bootstrap = {
|
||||
description = "Bootstrap RabbitMQ user for Burrow Zulip";
|
||||
after = [ "rabbitmq.service" ];
|
||||
wants = [ "rabbitmq.service" ];
|
||||
requiredBy = [ "burrow-zulip.service" ];
|
||||
before = [ "burrow-zulip.service" ];
|
||||
path = [
|
||||
config.services.rabbitmq.package
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.gawk
|
||||
pkgs.gnugrep
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "root";
|
||||
Group = "root";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
rabbit_password="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.rabbitmqPasswordFile})"
|
||||
export HOME=${config.services.rabbitmq.dataDir}
|
||||
|
||||
rabbitmqctl await_startup
|
||||
|
||||
if rabbitmqctl list_users -q | awk '{ print $1 }' | grep -qx zulip; then
|
||||
rabbitmqctl change_password zulip "$rabbit_password"
|
||||
else
|
||||
rabbitmqctl add_user zulip "$rabbit_password"
|
||||
fi
|
||||
|
||||
rabbitmqctl set_permissions -p / zulip '.*' '.*' '.*'
|
||||
|
||||
if rabbitmqctl list_users -q | awk '{ print $1 }' | grep -qx guest; then
|
||||
rabbitmqctl delete_user guest
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.burrow-zulip-runtime = {
|
||||
description = "Prepare Burrow Zulip compose and SAML runtime files";
|
||||
after = [
|
||||
"postgresql.service"
|
||||
"redis-zulip.service"
|
||||
"memcached.service"
|
||||
"rabbitmq.service"
|
||||
"burrow-zulip-postgres-bootstrap.service"
|
||||
"burrow-zulip-rabbitmq-bootstrap.service"
|
||||
"burrow-authentik-ready.service"
|
||||
"burrow-authentik-zulip-saml.service"
|
||||
"network-online.target"
|
||||
];
|
||||
wants = [
|
||||
"postgresql.service"
|
||||
"redis-zulip.service"
|
||||
"memcached.service"
|
||||
"rabbitmq.service"
|
||||
"burrow-zulip-postgres-bootstrap.service"
|
||||
"burrow-zulip-rabbitmq-bootstrap.service"
|
||||
"burrow-authentik-ready.service"
|
||||
"burrow-authentik-zulip-saml.service"
|
||||
"network-online.target"
|
||||
];
|
||||
requiredBy = [ "burrow-zulip.service" ];
|
||||
before = [ "burrow-zulip.service" ];
|
||||
path = [
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.curl
|
||||
pkgs.python3
|
||||
];
|
||||
restartTriggers = [
|
||||
composeFile
|
||||
cfg.postgresPasswordFile
|
||||
cfg.rabbitmqPasswordFile
|
||||
cfg.redisPasswordFile
|
||||
cfg.secretKeyFile
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "root";
|
||||
Group = "root";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
install -d -m 0755 ${lib.escapeShellArg cfg.dataDir}
|
||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data"}
|
||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data/logs"}
|
||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/data/logs/emails"}
|
||||
install -d -m 0700 ${lib.escapeShellArg "${cfg.dataDir}/data/secrets"}
|
||||
install -d -m 0700 ${lib.escapeShellArg "${cfg.dataDir}/secrets"}
|
||||
install -d -m 0755 ${lib.escapeShellArg "${cfg.dataDir}/logs"}
|
||||
install -m 0644 ${composeFile} ${lib.escapeShellArg "${cfg.dataDir}/compose.yaml"}
|
||||
: > ${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"} <<EOF
|
||||
secrets:
|
||||
zulip__postgres_password:
|
||||
file: ${cfg.postgresPasswordFile}
|
||||
zulip__rabbitmq_password:
|
||||
file: ${cfg.rabbitmqPasswordFile}
|
||||
zulip__redis_password:
|
||||
file: ${cfg.redisPasswordFile}
|
||||
zulip__secret_key:
|
||||
file: ${cfg.secretKeyFile}
|
||||
zulip__email_password:
|
||||
file: ${cfg.dataDir}/secrets/email-password
|
||||
|
||||
services:
|
||||
zulip:
|
||||
environment:
|
||||
SETTING_EXTERNAL_HOST: "${cfg.domain}"
|
||||
SETTING_ZULIP_ADMINISTRATOR: "${cfg.administratorEmail}"
|
||||
TRUST_GATEWAY_IP: "True"
|
||||
SETTING_SEND_LOGIN_EMAILS: "False"
|
||||
ZULIP_AUTH_BACKENDS: "EmailAuthBackend,SAMLAuthBackend"
|
||||
CONFIG_application_server__http_only: true
|
||||
CONFIG_application_server__nginx_listen_port: ${toString cfg.port}
|
||||
CONFIG_application_server__queue_workers_multiprocess: false
|
||||
ZULIP_CUSTOM_SETTINGS: |
|
||||
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
|
||||
EMAIL_FILE_PATH = "/data/logs/emails"
|
||||
SOCIAL_AUTH_SAML_ORG_INFO = {
|
||||
"en-US": {
|
||||
"displayname": "Burrow Zulip",
|
||||
"name": "zulip",
|
||||
"url": "https://${cfg.domain}",
|
||||
},
|
||||
}
|
||||
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
|
||||
"authentik": {
|
||||
"entity_id": "https://${cfg.authentikDomain}",
|
||||
"url": "https://${cfg.authentikDomain}/application/saml/${cfg.authentikProviderSlug}/sso/binding/redirect/",
|
||||
"display_name": "burrow.net",
|
||||
"x509cert": """$saml_cert""",
|
||||
"attr_user_permanent_id": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
"attr_username": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
"attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
"attr_first_name": "firstName",
|
||||
"attr_last_name": "lastName",
|
||||
},
|
||||
}
|
||||
EOF
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.burrow-zulip = {
|
||||
description = "Run Burrow Zulip with host-managed dependencies";
|
||||
after = [
|
||||
"burrow-zulip-runtime.service"
|
||||
"network-online.target"
|
||||
];
|
||||
wants = [
|
||||
"burrow-zulip-runtime.service"
|
||||
"network-online.target"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.gawk
|
||||
pkgs.gnugrep
|
||||
pkgs.openssl
|
||||
pkgs.podman
|
||||
pkgs.podman-compose
|
||||
];
|
||||
restartTriggers = [
|
||||
composeFile
|
||||
cfg.postgresPasswordFile
|
||||
cfg.rabbitmqPasswordFile
|
||||
cfg.redisPasswordFile
|
||||
cfg.secretKeyFile
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "root";
|
||||
Group = "root";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
RemainAfterExit = true;
|
||||
TimeoutStopSec = "20s";
|
||||
ExecStop = "${pkgs.bash}/bin/bash -lc 'set -euo pipefail; if ${pkgs.podman}/bin/podman container exists burrow-zulip_zulip_1; then ${pkgs.podman}/bin/podman stop --ignore --time 10 burrow-zulip_zulip_1 >/dev/null || true; ${pkgs.podman}/bin/podman rm -f --ignore burrow-zulip_zulip_1 >/dev/null || true; fi'";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
cd ${lib.escapeShellArg cfg.dataDir}
|
||||
|
||||
compose() {
|
||||
${pkgs.podman-compose}/bin/podman-compose -p burrow-zulip "$@"
|
||||
}
|
||||
|
||||
ensure_zulip_data_layout() {
|
||||
local zulip_data_dir=${lib.escapeShellArg "${cfg.dataDir}/data"}
|
||||
|
||||
install -d -m 0755 "$zulip_data_dir/logs"
|
||||
install -d -m 0755 "$zulip_data_dir/logs/emails"
|
||||
install -d -m 0700 "$zulip_data_dir/secrets"
|
||||
chown 1000:1000 "$zulip_data_dir/logs" "$zulip_data_dir/logs/emails" "$zulip_data_dir/secrets"
|
||||
|
||||
if [ ! -s "$zulip_data_dir/secrets/bootstrap-owner-password" ]; then
|
||||
umask 077
|
||||
openssl rand -base64 24 > "$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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
¨¤ãö9a¿ª5ÌIµÛÙƒçŽèV[fÀÁàç,Dàb \æv&§L½Z7õ!åû?4=JxFíÁeVÔ
|
||||
Binary file not shown.
|
|
@ -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
|
||||
˜:Š,+!°¬›ÏÛöϨϬB4DÿŸmHè÷®|Ä(çŸø9ñ‹l9†LßPZ^ïzed=im¡óëz¢æ?øŸ
|
||||
|
|
@ -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
|
||||
Óí¯FžoQ˜˜ƒ]ÈŸ‹õ‘ˆ‚þš‰ûëeU³/óíÑ/ó®ÂnÀoø.XþGù£J•й|‡<>õ+ïž
|
||||
|
|
@ -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
|
||||
÷¼âM¬kÜf"¹¬ž(qëxöÅèF2Bd…ÖMRîYji ¯õˆÖ°Ü´Å<¿òøÒ‘¾™Ñí¯b_.!r+¸<ÀÞUsž²sëô<C3AB>Êæ<C38A>uí?g«ÉDçã\Öðú<C3B0>VaÊÓÄÓ¨mÇã(ȈÞ&.ñ&Àc/|½w˜²ƒ(Wð’¡ÁýïªHޝœ4rÑ ¾¸á°æ+ j"üñÙB §
|
||||
Loading…
Add table
Add a link
Reference in a new issue