chore(authentik): tear down obsolete tripit-enrollment (ADR-0020 superseded by ADR-0028)
All checks were successful
ci/woodpecker/push/default Pipeline was successful
All checks were successful
ci/woodpecker/push/default Pipeline was successful
TripIt external users are now LOCAL TripIt accounts (ADR-0028 native passkey + Authentik OIDC), so the Authentik-side self-enrollment machinery is dead. Removes the tripit-enrollment + tripit-recovery flows and all their stages/prompts/policies/bindings, the tripit-email-stages blueprint (+yaml), and the 'TripIt External' group; reverts the admin-services-restriction fence branch that contained those users (its sole member, the leftover tripit-demo@ test account, was deleted first, so the revert affects zero live principals). Real external collaborators (type=external) are untouched. tg plan: 0 add, 1 change (the policy expression), 20 destroy (all tripit_*). Closes tripit#97; moots the B2 per-app OIDC fences. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
834c5e6a2a
commit
3278588325
7 changed files with 0 additions and 564 deletions
|
|
@ -49,21 +49,6 @@ resource "authentik_policy_expression" "admin_services_restriction" {
|
|||
|
||||
host = request.context.get("host", "")
|
||||
|
||||
# TripIt External containment fence (ADR-0020 in the tripit repo). Publicly
|
||||
# self-enrolled TripIt users (group "TripIt External", assigned by the
|
||||
# tripit-enrollment flow's user_write) may reach tripit.viktorbarzin.me and
|
||||
# NOTHING else. MUST be the FIRST host-dispatch branch: it is a request.user
|
||||
# predicate that must dominate every host branch below, ESPECIALLY the
|
||||
# default-allow `if host not in ADMIN_ONLY_HOSTS: return True` — placed after
|
||||
# it, a tagged user would slip into other hosts. Safe to add: the group is
|
||||
# net-new and created EMPTY, so this matches zero existing principals (no
|
||||
# lockout). The fence is forward-auth ONLY; OIDC apps (Vault, Immich, …)
|
||||
# contain External users via their own per-app group bindings — see
|
||||
# docs/runbooks/tripit-external-signup.md. NEVER co-assign "TripIt External"
|
||||
# to a trusted/internal user (this branch would fence them out of admin hosts).
|
||||
if ak_is_group_member(request.user, name="TripIt External"):
|
||||
return host == "tripit.viktorbarzin.me"
|
||||
|
||||
# t3 Workstation edge gate: only members of "T3 Users" may reach t3.
|
||||
# Placed BEFORE the ADMIN_ONLY_HOSTS early-return (t3 is intentionally not in
|
||||
# that set — it must not require Home-Server-Admins, just T3 Users membership).
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# Delivers the TripIt enrollment/recovery email-verification stages + their flow
|
||||
# bindings (tripit-email-stages.yaml) as a server-applied Authentik blueprint.
|
||||
#
|
||||
# Why a blueprint and not authentik_stage_email resources: the globally-pinned
|
||||
# provider (goauthentik 2024.x in terragrunt.hcl) models EmailStage.token_expiry
|
||||
# as an integer, but the live server (2026.2.x) requires a duration string and
|
||||
# 400s any number. The blueprint is parsed by the server, which accepts the
|
||||
# string. Bumping the provider would mean a global terragrunt.hcl change that
|
||||
# re-applies every platform stack — disproportionate. See tripit-flows.tf.
|
||||
#
|
||||
# depends_on the flows so they exist before Authentik resolves the blueprint's
|
||||
# !Find [..., slug, tripit-enrollment|tripit-recovery] references.
|
||||
resource "authentik_blueprint" "tripit_email_stages" {
|
||||
name = "tripit-email-stages"
|
||||
content = file("${path.module}/tripit-email-stages.yaml")
|
||||
enabled = true
|
||||
|
||||
depends_on = [
|
||||
authentik_flow.tripit_enrollment,
|
||||
authentik_flow.tripit_recovery,
|
||||
]
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# TripIt enrollment + recovery email-verification stages (tripit ADR-0020).
|
||||
#
|
||||
# Delivered as an Authentik blueprint (applied server-side by Authentik) instead
|
||||
# of via the Terraform provider, because the globally-pinned provider
|
||||
# (goauthentik 2024.x) models EmailStage.token_expiry as an integer while the
|
||||
# live server (2026.2.x) requires a duration string ("hours=24") and rejects any
|
||||
# number. See tripit-flows.tf for the rest of the flow and tripit-email-blueprint.tf
|
||||
# for the delivery resource.
|
||||
#
|
||||
# These two stages + their flow bindings are the SECURITY BOUNDARY of ADR-0020:
|
||||
# enrollment creates the user inactive; only clicking the link from
|
||||
# tripit-enrollment-verify (activate_user_on_success) makes the account usable.
|
||||
# Without this, anyone could self-enroll under an address they don't control and
|
||||
# tripit (which trusts X-authentik-email) would treat them as that identity.
|
||||
version: 1
|
||||
metadata:
|
||||
name: tripit-email-stages
|
||||
entries:
|
||||
# Enrollment: verify the email and ACTIVATE the (initially inactive) user.
|
||||
- model: authentik_stages_email.emailstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: tripit-enrollment-verify
|
||||
attrs:
|
||||
use_global_settings: true # noreply@viktorbarzin.me via mail.viktorbarzin.me
|
||||
activate_user_on_success: true
|
||||
subject: Confirm your TripIt account
|
||||
template: email/account_confirmation.html
|
||||
token_expiry: hours=24
|
||||
# Recovery: prove inbox ownership before letting the user register a new passkey.
|
||||
- model: authentik_stages_email.emailstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: tripit-recovery-email
|
||||
attrs:
|
||||
use_global_settings: true
|
||||
activate_user_on_success: false
|
||||
subject: Recover your TripIt access
|
||||
template: email/account_confirmation.html
|
||||
token_expiry: hours=1
|
||||
# Bind enrollment-verify into tripit-enrollment at order 30
|
||||
# (prompt 10 -> write 20 -> VERIFY 30 -> passkey 40 -> login 50).
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, tripit-enrollment]]
|
||||
stage: !Find [authentik_stages_email.emailstage, [name, tripit-enrollment-verify]]
|
||||
attrs:
|
||||
order: 30
|
||||
# Bind recovery-email into tripit-recovery at order 20
|
||||
# (identify 10 -> EMAIL 20 -> new passkey 30 -> login 40).
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, tripit-recovery]]
|
||||
stage: !Find [authentik_stages_email.emailstage, [name, tripit-recovery-email]]
|
||||
attrs:
|
||||
order: 20
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# "TripIt External" group — containment anchor for publicly self-enrolled TripIt
|
||||
# users (ADR-0020 in the tripit repo). Members are admitted to
|
||||
# tripit.viktorbarzin.me ONLY and denied every other *.viktorbarzin.me
|
||||
# forward-auth host by the prepended branch in admin-services-restriction.tf.
|
||||
#
|
||||
# Created EMPTY and PARENTLESS, on purpose:
|
||||
# * EMPTY — the no-lockout guarantee. Zero members at apply time => the
|
||||
# prepended policy branch matches zero existing principals => it cannot
|
||||
# change anyone's authorization (contrast authentik_group "T3 Users", which
|
||||
# is created WITH members atomically because THAT gate's safety property is
|
||||
# the opposite). Membership is assigned at RUNTIME by the tripit-enrollment
|
||||
# flow's user_write "Create users group" option (authentik_stage_user_write
|
||||
# in tripit-flows.tf). Terraform owns the group's EXISTENCE and the flow that
|
||||
# assigns it.
|
||||
# * PARENTLESS — do NOT make this a child of "Allow Login Users". The sensitive
|
||||
# OIDC apps gate on "Home Server Admins" / "Headscale Users" / "Wrongmove
|
||||
# Users" (children of "Allow Login Users") or, for Vault, on "Allow Login
|
||||
# Users" itself (bound as part of ADR-0020). Keeping External out of that
|
||||
# tree is what stops these users reaching OIDC apps — mirrors guest.tf, which
|
||||
# keeps the guest group out of "Allow Login Users" for the same reason.
|
||||
resource "authentik_group" "tripit_external" {
|
||||
name = "TripIt External"
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
# =============================================================================
|
||||
# TripIt external-user self-service flows (tripit ADR-0020).
|
||||
#
|
||||
# Public, passwordless self-signup for external users (people Viktor shares
|
||||
# trips with). Three concerns:
|
||||
#
|
||||
# * tripit-enrollment — open registration with email + passkey. Creates an
|
||||
# INACTIVE external user in "TripIt External", then an email-verification
|
||||
# stage ACTIVATES it. Email verification is the SECURITY BOUNDARY: tripit's
|
||||
# backend trusts the X-authentik-email header (AUTH_MODE=hybrid), so a user
|
||||
# must not be able to enroll under an address they don't control. Creating
|
||||
# the user inactive and activating ONLY on a clicked verification link
|
||||
# enforces that — an attacker who enters someone else's address produces an
|
||||
# inactive account that never activates and can never log in.
|
||||
#
|
||||
# * passwordless login — already provided by the built-in `webauthn` flow,
|
||||
# wired as passwordless_flow on the default login page's identification
|
||||
# stage. No new flow needed; external users with a passkey log in there.
|
||||
#
|
||||
# * tripit-recovery — email-anchored: prove inbox ownership, then register a
|
||||
# NEW passkey (the "lost my device" path; multi-passkey per ADR-0020). NOT
|
||||
# wired into the brand/global login flow (that would change ADMIN recovery
|
||||
# behaviour) — reached via its own /if/flow/tripit-recovery/ URL.
|
||||
#
|
||||
# Fence: the "TripIt External" group (tripit-external.tf) + the prepended branch
|
||||
# in admin-services-restriction.tf admit these users to tripit.viktorbarzin.me
|
||||
# ONLY and deny every other *.viktorbarzin.me forward-auth host. Email is sent
|
||||
# via the global SMTP settings wired in modules/authentik/values.yaml
|
||||
# (noreply@viktorbarzin.me through mail.viktorbarzin.me).
|
||||
# =============================================================================
|
||||
|
||||
# ---- Shared stages (used by both enrollment and recovery) -------------------
|
||||
|
||||
# Discoverable (resident) credential => usernameless passwordless login via the
|
||||
# built-in `webauthn` flow, which has no identification stage and so REQUIRES a
|
||||
# discoverable credential to resolve the user. "required" guarantees that;
|
||||
# every modern passkey authenticator supports it.
|
||||
resource "authentik_stage_authenticator_webauthn" "tripit_passkey" {
|
||||
name = "tripit-passkey-setup"
|
||||
resident_key_requirement = "required"
|
||||
user_verification = "preferred"
|
||||
}
|
||||
|
||||
resource "authentik_stage_user_login" "tripit_login" {
|
||||
name = "tripit-login"
|
||||
session_duration = "weeks=4"
|
||||
}
|
||||
|
||||
# ---- Enrollment -------------------------------------------------------------
|
||||
|
||||
resource "authentik_stage_prompt_field" "tripit_enroll_email" {
|
||||
name = "tripit-enroll-email"
|
||||
field_key = "email"
|
||||
label = "Email"
|
||||
type = "email"
|
||||
required = true
|
||||
order = 0
|
||||
}
|
||||
|
||||
resource "authentik_stage_prompt_field" "tripit_enroll_name" {
|
||||
name = "tripit-enroll-name"
|
||||
field_key = "name"
|
||||
label = "Full name"
|
||||
type = "text"
|
||||
required = true
|
||||
order = 1
|
||||
}
|
||||
|
||||
resource "authentik_stage_prompt" "tripit_enroll_prompt" {
|
||||
name = "tripit-enrollment-prompt"
|
||||
fields = [
|
||||
authentik_stage_prompt_field.tripit_enroll_email.id,
|
||||
authentik_stage_prompt_field.tripit_enroll_name.id,
|
||||
]
|
||||
}
|
||||
|
||||
resource "authentik_stage_user_write" "tripit_enroll_write" {
|
||||
name = "tripit-enrollment-write"
|
||||
# Created INACTIVE: only the email-verification stage (below) activates it.
|
||||
create_users_as_inactive = true
|
||||
# Land in the fenced group (tripit-only via admin-services-restriction).
|
||||
create_users_group = authentik_group.tripit_external.id
|
||||
user_type = "external"
|
||||
# Open registration: ALWAYS create a fresh user; never attach to / mutate an
|
||||
# existing one. There is no identification stage before this, so there is no
|
||||
# pending user to hijack — this is belt-and-suspenders against account takeover.
|
||||
user_creation_mode = "always_create"
|
||||
}
|
||||
|
||||
# NOTE: the two email-verification stages (enrollment + recovery) AND their flow
|
||||
# bindings are deliberately NOT defined here — they live in an Authentik
|
||||
# BLUEPRINT (tripit-email-blueprint.tf), applied server-side. Reason: the
|
||||
# globally-pinned provider (goauthentik 2024.x, terragrunt.hcl) models
|
||||
# EmailStage.token_expiry as an INTEGER, but the live server (2026.2.x) requires
|
||||
# a duration STRING ("hours=24") and 400s any number — the provider cannot send
|
||||
# a valid value (confirmed: even the unset default `30` is rejected). The
|
||||
# blueprint is parsed by the server, which accepts the string. Bumping the
|
||||
# provider would be a global terragrunt.hcl change that re-applies every platform
|
||||
# stack and breaks 3 other authentik-using app stacks' lockfiles — out of all
|
||||
# proportion to two stages. See tripit ADR-0020.
|
||||
|
||||
resource "authentik_flow" "tripit_enrollment" {
|
||||
name = "Sign up for TripIt"
|
||||
title = "Create your TripIt account"
|
||||
slug = "tripit-enrollment"
|
||||
designation = "enrollment"
|
||||
authentication = "require_unauthenticated"
|
||||
}
|
||||
|
||||
# prompt -> write(inactive) -> verify(activate) -> passkey -> login
|
||||
resource "authentik_flow_stage_binding" "tripit_enroll_10_prompt" {
|
||||
target = authentik_flow.tripit_enrollment.uuid
|
||||
stage = authentik_stage_prompt.tripit_enroll_prompt.id
|
||||
order = 10
|
||||
}
|
||||
resource "authentik_flow_stage_binding" "tripit_enroll_20_write" {
|
||||
target = authentik_flow.tripit_enrollment.uuid
|
||||
stage = authentik_stage_user_write.tripit_enroll_write.id
|
||||
order = 20
|
||||
# Run the username-from-email policy (below) at stage-execution time, when
|
||||
# prompt_data is populated — not at plan time. Mirrors guest.tf's pre-stage
|
||||
# context-mutation pattern.
|
||||
evaluate_on_plan = false
|
||||
re_evaluate_policies = true
|
||||
}
|
||||
|
||||
# Passwordless, email-only signup collects no username, but user_write aborts on
|
||||
# an empty username ("Aborting write to empty username"). Derive the username
|
||||
# from the entered email just before user_write runs. Mutating flow_plan.context
|
||||
# is the canonical mutable path — a plain request.context mutation would not
|
||||
# propagate to the stage (see guest.tf's pending_user note).
|
||||
resource "authentik_policy_expression" "tripit_username_from_email" {
|
||||
name = "tripit-enrollment-username-from-email"
|
||||
expression = trimspace(<<-EOT
|
||||
pd = request.context["flow_plan"].context.setdefault("prompt_data", {})
|
||||
pd["username"] = pd.get("email", "")
|
||||
return True
|
||||
EOT
|
||||
)
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "tripit_username_before_write" {
|
||||
target = authentik_flow_stage_binding.tripit_enroll_20_write.id
|
||||
policy = authentik_policy_expression.tripit_username_from_email.id
|
||||
order = 0
|
||||
}
|
||||
|
||||
# order 30 (email-verification binding) is in tripit-email-blueprint.tf — see note above
|
||||
resource "authentik_flow_stage_binding" "tripit_enroll_40_passkey" {
|
||||
target = authentik_flow.tripit_enrollment.uuid
|
||||
stage = authentik_stage_authenticator_webauthn.tripit_passkey.id
|
||||
order = 40
|
||||
}
|
||||
resource "authentik_flow_stage_binding" "tripit_enroll_50_login" {
|
||||
target = authentik_flow.tripit_enrollment.uuid
|
||||
stage = authentik_stage_user_login.tripit_login.id
|
||||
order = 50
|
||||
}
|
||||
|
||||
# ---- Recovery (email-anchored, passwordless) --------------------------------
|
||||
|
||||
resource "authentik_stage_identification" "tripit_recover_ident" {
|
||||
name = "tripit-recovery-identification"
|
||||
user_fields = ["email"]
|
||||
# Anti-enumeration: proceed even for an unknown address (no "user not found").
|
||||
pretend_user_exists = true
|
||||
}
|
||||
|
||||
# (recovery email-verification stage is in tripit-email-blueprint.tf — see note above)
|
||||
|
||||
resource "authentik_flow" "tripit_recovery" {
|
||||
name = "Recover TripIt access"
|
||||
title = "Recover your TripIt account"
|
||||
slug = "tripit-recovery"
|
||||
designation = "recovery"
|
||||
authentication = "require_unauthenticated"
|
||||
}
|
||||
|
||||
# identify(email) -> email(prove ownership) -> new passkey -> login
|
||||
resource "authentik_flow_stage_binding" "tripit_recover_10_ident" {
|
||||
target = authentik_flow.tripit_recovery.uuid
|
||||
stage = authentik_stage_identification.tripit_recover_ident.id
|
||||
order = 10
|
||||
}
|
||||
# order 20 (email-verification binding) is in tripit-email-blueprint.tf — see note above
|
||||
resource "authentik_flow_stage_binding" "tripit_recover_30_passkey" {
|
||||
target = authentik_flow.tripit_recovery.uuid
|
||||
stage = authentik_stage_authenticator_webauthn.tripit_passkey.id
|
||||
order = 30
|
||||
}
|
||||
resource "authentik_flow_stage_binding" "tripit_recover_40_login" {
|
||||
target = authentik_flow.tripit_recovery.uuid
|
||||
stage = authentik_stage_user_login.tripit_login.id
|
||||
order = 40
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue