Merge remote-tracking branch 'origin/master' into wizard/claude-auth-renew

This commit is contained in:
Viktor Barzin 2026-06-20 20:10:48 +00:00
commit bc2fbc712c
7 changed files with 0 additions and 564 deletions

View file

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

View file

@ -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,
]
}

View file

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

View file

@ -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"
}

View file

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