Compare commits

..

No commits in common. "7d3e7a6ec56e1739526235a05d2c16857fc4cdfa" and "5a4fe58b86fbf70b1de85e8e1a61a75600bb5687" have entirely different histories.

9 changed files with 0 additions and 1335 deletions

View file

@ -1,243 +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_ONEPASSWORD_APPLICATION_SLUG:-onepassword}"
application_name="${AUTHENTIK_ONEPASSWORD_APPLICATION_NAME:-1Password}"
provider_name="${AUTHENTIK_ONEPASSWORD_PROVIDER_NAME:-1Password}"
template_slug="${AUTHENTIK_ONEPASSWORD_TEMPLATE_SLUG:-ts}"
client_id="${AUTHENTIK_ONEPASSWORD_CLIENT_ID:-1password.burrow.net}"
launch_url="${AUTHENTIK_ONEPASSWORD_LAUNCH_URL:-https://burrow-team.1password.com/}"
redirect_uris_json="${AUTHENTIK_ONEPASSWORD_REDIRECT_URIS_JSON:-[
\"https://burrow-team.1password.com/sso/oidc/redirect/\",
\"onepassword://sso/oidc/redirect\"
]}"
usage() {
cat <<'EOF'
Usage: Scripts/authentik-sync-1password-oidc.sh
Required environment:
AUTHENTIK_BOOTSTRAP_TOKEN
Optional environment:
AUTHENTIK_URL
AUTHENTIK_ONEPASSWORD_APPLICATION_SLUG
AUTHENTIK_ONEPASSWORD_APPLICATION_NAME
AUTHENTIK_ONEPASSWORD_PROVIDER_NAME
AUTHENTIK_ONEPASSWORD_TEMPLATE_SLUG
AUTHENTIK_ONEPASSWORD_CLIENT_ID
AUTHENTIK_ONEPASSWORD_LAUNCH_URL
AUTHENTIK_ONEPASSWORD_REDIRECT_URIS_JSON
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
if ! printf '%s' "$redirect_uris_json" | jq -e 'type == "array" and length > 0' >/dev/null; then
echo "error: AUTHENTIK_ONEPASSWORD_REDIRECT_URIS_JSON must be a non-empty JSON array" >&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
}
wait_for_authentik
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)' \
| head -n1
)"
if [[ -z "$template_provider" ]]; then
echo "error: could not resolve the Authentik OAuth provider template ${template_slug}" >&2
exit 1
fi
authorization_flow="$(printf '%s\n' "$template_provider" | jq -r '.authorization_flow')"
invalidation_flow="$(printf '%s\n' "$template_provider" | jq -r '.invalidation_flow')"
property_mappings="$(printf '%s\n' "$template_provider" | jq -c '.property_mappings')"
signing_key="$(printf '%s\n' "$template_provider" | jq -r '.signing_key')"
provider_payload="$(
jq -n \
--arg name "$provider_name" \
--arg authorization_flow "$authorization_flow" \
--arg invalidation_flow "$invalidation_flow" \
--arg client_id "$client_id" \
--arg signing_key "$signing_key" \
--argjson property_mappings "$property_mappings" \
--argjson redirect_uris "$redirect_uris_json" \
'{
name: $name,
authorization_flow: $authorization_flow,
invalidation_flow: $invalidation_flow,
client_type: "public",
client_id: $client_id,
include_claims_in_id_token: true,
redirect_uris: ($redirect_uris | map({matching_mode: "strict", url: .})),
property_mappings: $property_mappings,
signing_key: $signing_key,
issuer_mode: "per_provider",
sub_mode: "hashed_user_id"
}'
)"
existing_provider="$(
api GET "/api/v3/providers/oauth2/?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/oauth2/${provider_pk}/" "$provider_payload" >/dev/null
else
provider_pk="$(
api POST "/api/v3/providers/oauth2/" "$provider_payload" \
| jq -r '.pk // empty'
)"
fi
if [[ -z "${provider_pk:-}" ]]; then
echo "error: 1Password OIDC 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')"
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: 1Password OIDC application did not return a primary key" >&2
exit 1
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 1Password OIDC application ${application_slug} (${application_name})."
exit 0
fi
sleep 2
done
echo "warning: 1Password OIDC issuer document for ${application_slug} was not immediately readable; keeping reconciled config." >&2
echo "Synced Authentik 1Password OIDC application ${application_slug} (${application_name})."

View file

@ -1,344 +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_LINEAR_APPLICATION_SLUG:-linear}"
application_name="${AUTHENTIK_LINEAR_APPLICATION_NAME:-Linear}"
provider_name="${AUTHENTIK_LINEAR_PROVIDER_NAME:-Linear}"
launch_url="${AUTHENTIK_LINEAR_LAUNCH_URL:-https://linear.app/burrownet}"
acs_url="${AUTHENTIK_LINEAR_ACS_URL:-}"
audience="${AUTHENTIK_LINEAR_AUDIENCE:-}"
issuer="${AUTHENTIK_LINEAR_ISSUER:-${authentik_url}/application/saml/${application_slug}/metadata/}"
default_relay_state="${AUTHENTIK_LINEAR_DEFAULT_RELAY_STATE:-}"
usage() {
cat <<'EOF'
Usage: Scripts/authentik-sync-linear-saml.sh
Required environment:
AUTHENTIK_BOOTSTRAP_TOKEN
AUTHENTIK_LINEAR_ACS_URL
AUTHENTIK_LINEAR_AUDIENCE
Optional environment:
AUTHENTIK_URL
AUTHENTIK_LINEAR_APPLICATION_SLUG
AUTHENTIK_LINEAR_APPLICATION_NAME
AUTHENTIK_LINEAR_PROVIDER_NAME
AUTHENTIK_LINEAR_LAUNCH_URL
AUTHENTIK_LINEAR_ISSUER
AUTHENTIK_LINEAR_DEFAULT_RELAY_STATE
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
if [[ -z "$acs_url" ]]; then
echo "error: AUTHENTIK_LINEAR_ACS_URL is required" >&2
exit 1
fi
if [[ -z "$audience" ]]; then
echo "error: AUTHENTIK_LINEAR_AUDIENCE 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
}
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 Linear SAML Email" \
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" \
"email" \
'return request.user.email'
)"
name_mapping_pk="$(
reconcile_property_mapping \
"Burrow Linear SAML Name" \
"name" \
"name" \
'return request.user.name or request.user.username'
)"
first_name_mapping_pk="$(
reconcile_property_mapping \
"Burrow Linear 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 Linear 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 Linear 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 default_relay_state "$default_relay_state" \
--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
]
}
+ (if $default_relay_state == "" then {} else {default_relay_state: $default_relay_state} end)'
)"
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: Linear 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="existing"
api PATCH "/api/v3/core/applications/${application_slug}/" "$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: Linear SAML application did not return a primary key" >&2
exit 1
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 Linear SAML application ${application_slug} (${application_name})."
exit 0
;;
esac
sleep 2
done
echo "warning: Linear SAML metadata for ${application_slug} was not immediately readable; keeping reconciled config." >&2
echo "Synced Authentik Linear SAML application ${application_slug} (${application_name})."

View file

@ -1,311 +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_LINEAR_APPLICATION_SLUG:-linear}"
provider_name="${AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME:-Linear SCIM}"
scim_url="${AUTHENTIK_LINEAR_SCIM_URL:-}"
scim_token_file="${AUTHENTIK_LINEAR_SCIM_TOKEN_FILE:-}"
user_identifier="${AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER:-email}"
owner_group="${AUTHENTIK_LINEAR_OWNER_GROUP:-linear-owners}"
admin_group="${AUTHENTIK_LINEAR_ADMIN_GROUP:-linear-admins}"
guest_group="${AUTHENTIK_LINEAR_GUEST_GROUP:-linear-guests}"
usage() {
cat <<'EOF'
Usage: Scripts/authentik-sync-linear-scim.sh
Required environment:
AUTHENTIK_BOOTSTRAP_TOKEN
AUTHENTIK_LINEAR_SCIM_URL
AUTHENTIK_LINEAR_SCIM_TOKEN_FILE
Optional environment:
AUTHENTIK_URL
AUTHENTIK_LINEAR_APPLICATION_SLUG
AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME
AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER
AUTHENTIK_LINEAR_OWNER_GROUP
AUTHENTIK_LINEAR_ADMIN_GROUP
AUTHENTIK_LINEAR_GUEST_GROUP
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
if [[ -z "$scim_url" ]]; then
echo "error: AUTHENTIK_LINEAR_SCIM_URL is required" >&2
exit 1
fi
if [[ -z "$scim_token_file" || ! -s "$scim_token_file" ]]; then
echo "error: AUTHENTIK_LINEAR_SCIM_TOKEN_FILE is required and must be readable" >&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
}
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_group_pk() {
local group_name="$1"
api GET "/api/v3/core/groups/?page_size=200&search=${group_name}" \
| jq -r --arg name "$group_name" '.results[]? | select(.name == $name) | .pk // empty' \
| head -n1
}
ensure_group() {
local group_name="$1"
local payload group_pk
payload="$(jq -cn --arg name "$group_name" '{name: $name}')"
group_pk="$(lookup_group_pk "$group_name")"
if [[ -n "$group_pk" ]]; then
api PATCH "/api/v3/core/groups/${group_pk}/" "$payload" >/dev/null
else
group_pk="$(
api POST "/api/v3/core/groups/" "$payload" \
| jq -r '.pk // empty'
)"
fi
if [[ -z "$group_pk" ]]; then
echo "error: could not reconcile Authentik group ${group_name}" >&2
exit 1
fi
printf '%s\n' "$group_pk"
}
lookup_application() {
api GET "/api/v3/core/applications/?page_size=200" \
| jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \
| head -n1
}
lookup_scim_provider() {
api GET "/api/v3/providers/scim/?page_size=200" \
| jq -c \
--arg application_slug "$application_slug" \
--arg provider_name "$provider_name" \
'.results[]? | select(.assigned_backchannel_application_slug == $application_slug or .name == $provider_name)' \
| head -n1
}
lookup_scim_mapping_pk() {
local managed_name="$1"
api GET "/api/v3/propertymappings/provider/scim/?page_size=200" \
| jq -r --arg managed "$managed_name" '.results[]? | select(.managed == $managed) | .pk // empty' \
| head -n1
}
reconcile_property_mapping() {
local name="$1"
local expression="$2"
local payload existing_pk
payload="$(
jq -n \
--arg name "$name" \
--arg expression "$expression" \
'{
name: $name,
expression: $expression
}'
)"
existing_pk="$(
api GET "/api/v3/propertymappings/provider/scim/?page_size=200" \
| jq -r --arg name "$name" '.results[]? | select(.name == $name) | .pk // empty' \
| head -n1
)"
if [[ -n "$existing_pk" ]]; then
api PATCH "/api/v3/propertymappings/provider/scim/${existing_pk}/" "$payload" >/dev/null
printf '%s\n' "$existing_pk"
else
api POST "/api/v3/propertymappings/provider/scim/" "$payload" \
| jq -r '.pk // empty'
fi
}
sync_object() {
local provider_pk="$1"
local model="$2"
local object_id="$3"
if ! api POST "/api/v3/providers/scim/${provider_pk}/sync/object/" "$(
jq -cn \
--arg model "$model" \
--arg object_id "$object_id" \
'{
sync_object_model: $model,
sync_object_id: $object_id,
override_dry_run: false
}'
)" >/dev/null; then
echo "warning: could not trigger immediate Linear SCIM sync for ${model} ${object_id}; provider will continue with its normal sync cycle." >&2
fi
}
wait_for_authentik
group_mapping_pk="$(lookup_scim_mapping_pk "goauthentik.io/providers/scim/group")"
case "$user_identifier" in
email)
user_mapping_expression=$'# Some implementations require givenName and familyName to be set\ngivenName, familyName = request.user.name, " "\nformatted = request.user.name + " "\nif " " in request.user.name:\n givenName, _, familyName = request.user.name.partition(" ")\n formatted = request.user.name\n\navatar = request.user.avatar\nphotos = None\nif "://" in avatar:\n photos = [{"value": avatar, "type": "photo"}]\n\nlocale = request.user.locale()\nif locale == "":\n locale = None\n\nemails = []\nif request.user.email != "":\n emails = [{\n "value": request.user.email,\n "type": "other",\n "primary": True,\n }]\n\nidentifier = request.user.email\nif identifier == "":\n identifier = request.user.username\n\nreturn {\n "userName": identifier,\n "name": {\n "formatted": formatted,\n "givenName": givenName,\n "familyName": familyName,\n },\n "displayName": request.user.name,\n "photos": photos,\n "locale": locale,\n "active": request.user.is_active,\n "emails": emails,\n}'
;;
username)
user_mapping_expression=$'# Some implementations require givenName and familyName to be set\ngivenName, familyName = request.user.name, " "\nformatted = request.user.name + " "\nif " " in request.user.name:\n givenName, _, familyName = request.user.name.partition(" ")\n formatted = request.user.name\n\navatar = request.user.avatar\nphotos = None\nif "://" in avatar:\n photos = [{"value": avatar, "type": "photo"}]\n\nlocale = request.user.locale()\nif locale == "":\n locale = None\n\nemails = []\nif request.user.email != "":\n emails = [{\n "value": request.user.email,\n "type": "other",\n "primary": True,\n }]\nreturn {\n "userName": request.user.username,\n "name": {\n "formatted": formatted,\n "givenName": givenName,\n "familyName": familyName,\n },\n "displayName": request.user.name,\n "photos": photos,\n "locale": locale,\n "active": request.user.is_active,\n "emails": emails,\n}'
;;
*)
echo "error: unsupported AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER value: ${user_identifier}" >&2
exit 1
;;
esac
user_mapping_pk="$(reconcile_property_mapping "Burrow Linear SCIM User" "$user_mapping_expression")"
if [[ -z "$user_mapping_pk" || -z "$group_mapping_pk" ]]; then
echo "error: could not resolve managed Authentik SCIM property mappings" >&2
exit 1
fi
owner_group_pk="$(ensure_group "$owner_group")"
admin_group_pk="$(ensure_group "$admin_group")"
guest_group_pk="$(ensure_group "$guest_group")"
provider_payload="$(
jq -n \
--arg name "$provider_name" \
--arg url "$scim_url" \
--arg token "$(tr -d '\r\n' < "$scim_token_file")" \
--arg user_mapping_pk "$user_mapping_pk" \
--arg group_mapping_pk "$group_mapping_pk" \
--arg owner_group_pk "$owner_group_pk" \
--arg admin_group_pk "$admin_group_pk" \
--arg guest_group_pk "$guest_group_pk" \
'{
name: $name,
url: $url,
token: $token,
auth_mode: "token",
verify_certificates: true,
compatibility_mode: "default",
property_mappings: [$user_mapping_pk],
property_mappings_group: [$group_mapping_pk],
group_filters: [
$owner_group_pk,
$admin_group_pk,
$guest_group_pk
],
dry_run: false
}'
)"
existing_provider="$(lookup_scim_provider)"
if [[ -n "$existing_provider" ]]; then
provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')"
api PATCH "/api/v3/providers/scim/${provider_pk}/" "$provider_payload" >/dev/null
else
provider_pk="$(
api POST "/api/v3/providers/scim/" "$provider_payload" \
| jq -r '.pk // empty'
)"
fi
if [[ -z "${provider_pk:-}" ]]; then
echo "error: Linear SCIM provider did not return a primary key" >&2
exit 1
fi
application="$(lookup_application)"
if [[ -z "$application" ]]; then
echo "error: could not resolve Authentik application ${application_slug}" >&2
exit 1
fi
application_payload="$(
printf '%s\n' "$application" \
| jq \
--arg provider_pk "$provider_pk" \
'{
name: .name,
slug: .slug,
provider: .provider,
backchannel_providers: ((.backchannel_providers // []) + [($provider_pk | tonumber)] | unique),
open_in_new_tab: .open_in_new_tab,
meta_launch_url: .meta_launch_url,
policy_engine_mode: .policy_engine_mode
}'
)"
api PATCH "/api/v3/core/applications/${application_slug}/" "$application_payload" >/dev/null
group_pks_json="$(jq -cn --arg owner "$owner_group_pk" --arg admin "$admin_group_pk" --arg guest "$guest_group_pk" '[$owner, $admin, $guest]')"
user_pks_json="$(
api GET "/api/v3/core/users/?page_size=200" \
| jq -c \
--argjson group_pks "$group_pks_json" \
'[.results[]?
| select(
([((.groups // [])[] | tostring)] as $user_groups
| ($group_pks | map(. as $wanted | ($user_groups | index($wanted)) != null) | any))
)
| .pk]'
)"
while IFS= read -r group_pk; do
[[ -z "$group_pk" ]] && continue
sync_object "$provider_pk" "authentik.core.models.Group" "$group_pk"
done < <(printf '%s\n' "$group_pks_json" | jq -r '.[]')
while IFS= read -r user_pk; do
[[ -z "$user_pk" ]] && continue
sync_object "$provider_pk" "authentik.core.models.User" "$user_pk"
done < <(printf '%s\n' "$user_pks_json" | jq -r '.[]')
status_json="$(api GET "/api/v3/providers/scim/${provider_pk}/sync/status/" || true)"
if ! printf '%s\n' "$status_json" | jq -e 'has("last_sync_status")' >/dev/null 2>&1; then
echo "warning: could not read Linear SCIM sync status for provider ${provider_pk}; keeping reconciled configuration." >&2
fi
echo "Synced Authentik Linear SCIM provider ${provider_name} (${provider_pk}) with groups ${owner_group}, ${admin_group}, ${guest_group}."

View file

@ -2,11 +2,6 @@
groups = {
users = "burrow-users";
admins = "burrow-admins";
linear = {
owners = "linear-owners";
admins = "linear-admins";
guests = "linear-guests";
};
};
identities = {

View file

@ -1,160 +0,0 @@
# `BEP-0008` - Authentik-Backed Team Chat and Workspace Identity
```text
Status: Draft
Proposal: BEP-0008
Authors: gpt-5.4
Coordinator: gpt-5.4
Reviewers: Pending
Constitution Sections: II, III, V
Implementation PRs: Pending
Decision Date: Pending
```
## Summary
Burrow should add a self-hosted team chat surface at `chat.burrow.net` and
continue the project-wide move toward Authentik as the identity authority for
external work systems. The immediate targets are a self-hosted Zulip
deployment rooted in Authentik SAML, a Linear SAML configuration when the
workspace plan supports it, and a 1Password Unlock-with-SSO deployment rooted
in the same Authentik-backed OIDC authority.
This keeps Burrow's day-to-day coordination surfaces aligned with the same
admin groups, canonical users, and secret-handling model already used for
Forgejo, Headscale, and Tailscale. It also avoids fragmenting login state
across vendor-native Google auth flows when Burrow already operates an IdP.
## Motivation
- Forge, Tailnet, operator identity, and Tailscale custom OIDC are already
rooted in Authentik. Team chat, work tracking, and password-manager access
should not become separate authority islands.
- Zulip provides a self-hosted chat system under Burrow's control, which fits
the constitution better than adding another hosted chat dependency.
- Linear remains a SaaS dependency, but its workspace access should still be
derived from Burrow-managed identities and domains when the vendor plan
exposes SAML configuration.
- 1Password Business is another external work surface where Burrow-controlled
identities are preferable to vendor-native Google-only auth. Its current
vendor flow is OIDC-based Unlock with SSO rather than SAML, so the proposal
needs to preserve protocol accuracy instead of flattening everything into
one SAML bucket.
- Burrow already has a canonical public identity registry and a secret-backed
external-email alias map. Reusing that structure is lower-risk than
inventing per-app user bootstrap logic.
## Detailed Design
- 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.
- 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.
- Add Authentik-managed SAML applications for:
- Zulip at `chat.burrow.net`
- Linear using Burrow's claimed domains and Authentik metadata
- Add an Authentik-managed SCIM backchannel for Linear so Burrow can push
role groups declaratively instead of hand-maintaining workspace roles.
- Add an Authentik-managed OIDC application for 1Password Business under the
Burrow team sign-in address.
- Treat Zulip and Linear as downstream applications of the same identity
authority, and treat 1Password as part of that same authority even though
its vendor protocol is OIDC rather than SAML. The source of truth remains:
- public identities and admin intent in `contributors.nix`
- private alias mappings and external accounts in agenix-encrypted secrets
- Keep app-specific configuration in dedicated reconciliation code or module
options instead of hand-edited UI state.
- Prefer service-specific reconciliation over ad hoc manual setup so rebuilds
and host replacement converge automatically.
- 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
second manual roster.
- Model 1Password according to the vendor's actual integration contract:
- OIDC Authorization Code Flow with PKCE
- public client rather than a confidential client
- no Burrow-side dependence on a stored client secret unless the vendor flow
changes
## Security and Operational Considerations
- Do not store external personal email mappings in public registry files.
Public tree data may include Burrow usernames and canonical `@burrow.net`
addresses, but external aliases must stay in encrypted secrets.
- Zulip internal service credentials, Django secret material, and any mail
credentials must have explicit storage and rotation paths.
- Linear SAML must not become Burrow's only admin recovery path. At least one
owner login path outside the enforced SAML flow should remain available until
rollout is proven.
- Linear SCIM group push should be role-scoped and explicit. Burrow should
avoid blanket ownership mapping unless that intent is recorded in the repo.
- 1Password Owners cannot be forced onto Unlock with SSO during initial setup.
Burrow should preserve the owner recovery path and treat OIDC rollout as a
scoped migration for non-owner users first.
- If Zulip is deployed without production-grade outbound email at first, that
limitation must be documented and treated as an operational constraint, not a
hidden assumption.
- Rollback should be straightforward:
- disable or stop the Zulip module
- remove the Authentik SAML apps
- remove the Authentik OIDC app used for 1Password if necessary
- leave the underlying Burrow identities unchanged
## Contributor Playbook
- Define the app and identity intent in the repository before modifying the
forge host.
- Add or update Nix modules so `burrow-forge` can rebuild Zulip and the
corresponding Authentik SAML configuration from the tree.
- 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
- users in Burrow admin groups receive the expected access on first login
- Record concrete evidence for:
- host deployment generation
- Authentik reconciliation success
- Zulip login success
- Linear SAML configuration state
- 1Password Unlock with SSO configuration state
## Alternatives Considered
- Use Zulip Cloud instead of self-hosting. Rejected because the ask is to host
chat under `chat.burrow.net`, and Burrow already operates a forge host with a
self-managed identity plane.
- Keep Linear on Google-native login. Rejected because it leaves Burrow work
access outside the project's operator and group model.
- Treat 1Password as a SAML app for consistency. Rejected because the live
vendor flow is OIDC and Burrow should not pretend otherwise in repo-owned
infrastructure.
- Add per-app manual Authentik configuration without repository automation.
Rejected because it violates Burrow's infrastructure-in-repo commitment.
## Impact on Other Work
- Extends Burrow's Authentik role from control-plane identity into team-work
surfaces.
- Introduces a persistent chat workload on the forge host, with resource and
monitoring implications.
- Creates a likely follow-up for SCIM or richer group synchronization if Linear
or Zulip role mapping needs to become fully declarative later.
- Adds a second OIDC relying party beyond Forgejo, Headscale, and Tailscale,
which raises the importance of keeping Burrow's Authentik scope mappings and
redirect handling consistent across applications.
## Decision
Pending.
## References
- `CONSTITUTION.md`
- `contributors.nix`
- `evolution/proposals/BEP-0004-hosted-mail-and-saas-identity.md`
- Authentik docs: SAML provider and metadata endpoints
- Zulip docs: SAML authentication and docker deployment
- Linear docs: SAML and access control
- 1Password docs: Unlock with SSO using OpenID Connect

View file

@ -3,7 +3,6 @@
let
contributors = import ../../../contributors.nix;
identities = contributors.identities;
linearGroups = contributors.groups.linear;
stripNewline = value: lib.replaceStrings [ "\n" ] [ "" ] value;
authentikPasswordSecretPath = identity:
if identity ? authentikPasswordSecret
@ -16,7 +15,6 @@ let
name = identity.displayName;
email = identity.canonicalEmail;
isAdmin = identity.isAdmin or false;
groups = lib.optionals (identity.isAdmin or false) [ linearGroups.owners ];
passwordFile = authentikPasswordSecretPath identity;
}
)
@ -113,12 +111,6 @@ in
group = "root";
mode = "0400";
};
age.secrets.burrowLinearScimToken = {
file = ../../../secrets/infra/linear-scim-token.age;
owner = "root";
group = "root";
mode = "0400";
};
age.secrets.burrowAuthentikGoogleClientId = {
file = ../../../secrets/infra/authentik-google-client-id.age;
owner = "root";
@ -215,15 +207,6 @@ in
userGroupName = contributors.groups.users;
adminGroupName = contributors.groups.admins;
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";
linearDefaultRelayState = "https://linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
linearScimUrl = "https://api.linear.app/auth/scim/d0ca13dc-ac41-4824-8aab-e0ca352fc3de";
linearScimTokenFile = config.age.secrets.burrowLinearScimToken.path;
linearScimUserIdentifier = "email";
linearOwnerGroupName = linearGroups.owners;
linearAdminGroupName = linearGroups.admins;
linearGuestGroupName = linearGroups.guests;
};
services.burrow.headscale = {

View file

@ -11,9 +11,6 @@ let
directorySyncScript = ../../Scripts/authentik-sync-burrow-directory.sh;
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
tailscaleOidcSyncScript = ../../Scripts/authentik-sync-tailscale-oidc.sh;
onePasswordOidcSyncScript = ../../Scripts/authentik-sync-1password-oidc.sh;
linearSamlSyncScript = ../../Scripts/authentik-sync-linear-saml.sh;
linearScimSyncScript = ../../Scripts/authentik-sync-linear-scim.sh;
googleSourceSyncScript = ../../Scripts/authentik-sync-google-source.sh;
tailnetAuthFlowSyncScript = ../../Scripts/authentik-sync-tailnet-auth-flow.sh;
authentikBlueprint = pkgs.writeText "burrow-authentik-blueprint.yaml" ''
@ -153,99 +150,6 @@ in
description = "Host-local file containing the Authentik Tailscale OIDC client secret.";
};
onePasswordDomain = lib.mkOption {
type = lib.types.str;
default = "burrow-team.1password.com";
description = "1Password team sign-in domain used for Burrow Unlock with SSO.";
};
onePasswordProviderSlug = lib.mkOption {
type = lib.types.str;
default = "onepassword";
description = "Authentik application slug for 1Password Unlock with SSO.";
};
onePasswordClientId = lib.mkOption {
type = lib.types.str;
default = "1password.burrow.net";
description = "Public OIDC client ID Authentik should present to 1Password.";
};
onePasswordRedirectUris = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"https://burrow-team.1password.com/sso/oidc/redirect/"
"onepassword://sso/oidc/redirect"
];
description = "Allowed 1Password OIDC redirect URIs.";
};
linearProviderSlug = lib.mkOption {
type = lib.types.str;
default = "linear";
description = "Authentik application slug for Linear SAML.";
};
linearAcsUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Linear SAML ACS URL.";
};
linearAudience = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Linear SAML audience/entity identifier.";
};
linearLaunchUrl = lib.mkOption {
type = lib.types.str;
default = "https://linear.app/burrownet";
description = "Linear workspace URL exposed in Authentik.";
};
linearDefaultRelayState = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional Linear relay state or login URL for IdP-initiated launches.";
};
linearScimUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Linear SCIM base connector URL.";
};
linearScimTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Host-local file containing the Linear SCIM bearer token.";
};
linearScimUserIdentifier = lib.mkOption {
type = lib.types.str;
default = "email";
description = "Linear SCIM unique identifier field for users.";
};
linearOwnerGroupName = lib.mkOption {
type = lib.types.str;
default = "linear-owners";
description = "Authentik group name that should map to Linear owners.";
};
linearAdminGroupName = lib.mkOption {
type = lib.types.str;
default = "linear-admins";
description = "Authentik group name that should map to Linear admins.";
};
linearGuestGroupName = lib.mkOption {
type = lib.types.str;
default = "linear-guests";
description = "Authentik group name that should map to Linear guests.";
};
forgejoClientId = lib.mkOption {
type = lib.types.str;
default = "git.burrow.net";
@ -814,153 +718,6 @@ EOF
'';
};
systemd.services.burrow-authentik-1password-oidc = {
description = "Reconcile the Burrow Authentik 1Password OIDC application";
after = [
"burrow-authentik-ready.service"
"network-online.target"
];
wants = [
"burrow-authentik-ready.service"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
onePasswordOidcSyncScript
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_ONEPASSWORD_APPLICATION_SLUG=${lib.escapeShellArg cfg.onePasswordProviderSlug}
export AUTHENTIK_ONEPASSWORD_APPLICATION_NAME=1Password
export AUTHENTIK_ONEPASSWORD_PROVIDER_NAME=1Password
export AUTHENTIK_ONEPASSWORD_TEMPLATE_SLUG=${lib.escapeShellArg cfg.headscaleProviderSlug}
export AUTHENTIK_ONEPASSWORD_CLIENT_ID=${lib.escapeShellArg cfg.onePasswordClientId}
export AUTHENTIK_ONEPASSWORD_LAUNCH_URL=https://${cfg.onePasswordDomain}/
export AUTHENTIK_ONEPASSWORD_REDIRECT_URIS_JSON='${builtins.toJSON cfg.onePasswordRedirectUris}'
${pkgs.bash}/bin/bash ${onePasswordOidcSyncScript}
'';
};
systemd.services.burrow-authentik-linear-saml = lib.mkIf (
cfg.linearAcsUrl != null && cfg.linearAudience != null
) {
description = "Reconcile the Burrow Authentik Linear SAML application";
after = [
"burrow-authentik-ready.service"
"network-online.target"
];
wants = [
"burrow-authentik-ready.service"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
linearSamlSyncScript
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_LINEAR_APPLICATION_SLUG=${lib.escapeShellArg cfg.linearProviderSlug}
export AUTHENTIK_LINEAR_APPLICATION_NAME=Linear
export AUTHENTIK_LINEAR_PROVIDER_NAME=Linear
export AUTHENTIK_LINEAR_ACS_URL=${lib.escapeShellArg cfg.linearAcsUrl}
export AUTHENTIK_LINEAR_AUDIENCE=${lib.escapeShellArg cfg.linearAudience}
export AUTHENTIK_LINEAR_LAUNCH_URL=${lib.escapeShellArg cfg.linearLaunchUrl}
${lib.optionalString (cfg.linearDefaultRelayState != null) ''
export AUTHENTIK_LINEAR_DEFAULT_RELAY_STATE=${lib.escapeShellArg cfg.linearDefaultRelayState}
''}
${pkgs.bash}/bin/bash ${linearSamlSyncScript}
'';
};
systemd.services.burrow-authentik-linear-scim = lib.mkIf (
cfg.linearScimUrl != null && cfg.linearScimTokenFile != null
) {
description = "Reconcile the Burrow Authentik Linear SCIM provider";
after = [
"burrow-authentik-ready.service"
"burrow-authentik-directory.service"
"burrow-authentik-linear-saml.service"
"network-online.target"
];
wants = [
"burrow-authentik-ready.service"
"burrow-authentik-directory.service"
"burrow-authentik-linear-saml.service"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
linearScimSyncScript
cfg.envFile
cfg.linearScimTokenFile
];
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_LINEAR_APPLICATION_SLUG=${lib.escapeShellArg cfg.linearProviderSlug}
export AUTHENTIK_LINEAR_SCIM_PROVIDER_NAME="Linear SCIM"
export AUTHENTIK_LINEAR_SCIM_URL=${lib.escapeShellArg cfg.linearScimUrl}
export AUTHENTIK_LINEAR_SCIM_TOKEN_FILE=${lib.escapeShellArg cfg.linearScimTokenFile}
export AUTHENTIK_LINEAR_SCIM_USER_IDENTIFIER=${lib.escapeShellArg cfg.linearScimUserIdentifier}
export AUTHENTIK_LINEAR_OWNER_GROUP=${lib.escapeShellArg cfg.linearOwnerGroupName}
export AUTHENTIK_LINEAR_ADMIN_GROUP=${lib.escapeShellArg cfg.linearAdminGroupName}
export AUTHENTIK_LINEAR_GUEST_GROUP=${lib.escapeShellArg cfg.linearGuestGroupName}
${pkgs.bash}/bin/bash ${linearScimSyncScript}
'';
};
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
encode gzip zstd
reverse_proxy 127.0.0.1:${toString cfg.port}

View file

@ -23,6 +23,5 @@ in
"secrets/infra/forgejo-nsc-dispatcher-config.age".publicKeys = burrowForgeRecipients;
"secrets/infra/forgejo-nsc-token.age".publicKeys = burrowForgeRecipients;
"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;
}

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q Tb3hxc6ZscCQpr7s8raup25FA8YAmq30jHZfOQp28Xs
L9YhaX9IVinud0IOs5K55ldGx82wjXHxnVBHZnRjiTA
-> ssh-ed25519 IrZmAg etIe6hWDP9YkqDFCWybnvsOh7h8YO+z3tKc95pG64lU
BT3rH5a+LJZWv2xtWPbMJGS2oM9v4mOI9WPmnHebiew
-> ssh-ed25519 0kWPgQ YpCf5m16VaKp7d+C3oF9MJQB/0xzCNtD7ODsTiV8t1o
xG8G/kSM+7VrWHm299A7fG/kBFnoiWZPiDZuldvimLw
-> X25519 ETltnMPR7lWbBWJvJKmNZhS7wqX0WCa4aNu8UKzxMVE
Ys57VNuclgvN1nJIrLjNrwekbosa7KK9lFt0PTpr/MQ
--- ZeUmSOf8+NycQAFRGCJHYcQvTJqSBIGKEOEdCnNfJbE
±<»qìª1.ïO_š×ÇÕ¤7AÛ·_@%/Â5Ël½7J÷Œõɵ<19>Ä<EFBFBD>üA xÆØûÐèüb "B