Commit Graph

684 Commits

Author SHA1 Message Date
chrisfu
a2a82bc495 fix(samba_dns): use admin credentials instead of machine Kerberos (-P)
All samba-tool dns commands were using -P (machine account Kerberos) which
silently failed with no error — tasks reported changed=0 but records were
never written. Switch to -U Administrator --password={{ samba_dns_admin_pass }}
which uses the vault-protected admin credentials that were already defined
but never wired up.

Also fix regex patterns in record parsers: samba-tool output uses `A: IP`
and `PTR: fqdn` format, not `A IP` / `PTR fqdn` (space-separated), so
updated regex_findall patterns to match `TYPE:\s+value`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:15:13 -07:00
chrisfu
e014fd5b71 feat(k3s): add pg.prole.org as CNPG postgres endpoint with split-horizon DNS
- knoe-db.yaml: switch to CNPG-managed TLS cert with serverAltDNSNames
  (pg.prole.org + knoe-db-rw cluster service) — removes static serverTLSSecret/serverCASecret
- dns.yml: add pg.prole.org A record to prole_k3s_dns_records (10.0.0.3, 10.0.0.6)
  for Ansible-managed split-horizon DNS via Samba AD DC
- k3s.cfg: align KNOE_HOME paths to ~/dev/prole, add PROLE_KDC_* vars, remove
  hardcoded KUBECTL_CONTEXT (kubeconfig current-context is authoritative)
- prod.cfg: add PROLE_KDC_STORAGE_CLASS = prole-iscsi
- onepassword.py: skip vault check gracefully when no 1Password session active (non-TTY)
- CLAUDE.md: document production postgres connection string and DNS/CA cert ops

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:46:46 -07:00
chrisfu
f34d10908a docs(knoe-auth): use [placeholder] instead of <placeholder> in mermaid
Mermaid 10.7.0 chokes on bare <placeholder> in message text, parsing
it as HTML. Swap to square brackets in the cross-realm §4.3 sequence
diagram and the Gitea SPNEGO §5 diagram:

  <svc-host>, <SPNEGO>          (§4.3)
  <blob>                        (§5)

Matches the same fix landed in knoe-db's docs/knoe-auth.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:31:22 -07:00
chrisfu
f1450605c2 docs: knoe-auth — prole.org deployment overlay
Companion to the canonical architecture doc at
~/dev/knoe-db/docs/knoe-auth.md. Documents what's specific to the
prole.org homelab deployment:

  - Two upstream identity layers (Samba AD on myrddin for PROLE.ORG
    Kerberos, Google Workspace knoey.com for OIDC)
  - Three deploy targets (k3s on myrddin/merlin/gandalf, k3d for
    engineer dev loop, GKE for production API endpoint)
  - Hostname map (myrddin/morgana/zinfandel/db.prole.org/
    git.prole.org/api.knoe.dev/db.knoe.dev/etc.)
  - Cross-realm Kerberos trust setup (KNOE.LOCAL ↔ PROLE.ORG) via
    infrastructure/playbooks/kerberos_trust_setup.yml — RC4 pin
    explanation, kvno sync notes
  - Gitea SPNEGO at git.prole.org with Mermaid flow diagram
  - Supabase Studio status (db.prole.org Google OAuth working;
    db.knoe.dev knoe-auth OIDC blocked on pg_oauth image)
  - Engineer onboarding checklist (prole-specific steps)
  - Known fragility / current incidents table

Mermaid sequence diagram covers the cross-realm flow end-to-end
(workstation → Samba KDC → MIT KDC → service ticket), distinct from
the canonical doc's generic version.

References the matching Junie briefs in knoe-db for the open
fragility items (pg_oauth-image-install, init-user-schemas-heredoc-fix,
deploy-pipeline-restore).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:19:14 -07:00
chrisfu
3f34fa8b32 fix(installer): k3s --reset path hardening (kdc deploy, no-TTY 1password, context overrides)
Five fixes Junie surfaced while running the kdc-trust-reset-repeatable
Junie brief end-to-end (companion to commit 6f99f95). All hit during
the unattended `install.sh --mode k3s --reset` pipeline.

  - knoe/core/milestones.py (KerberosMilestone):
    For k3s and k3d modes, deploy the KDC pod via `init_kdc.sh start`
    before running init_kerberos.sh. init_kerberos.sh only chains into
    init_kdc.sh when PROLE_KDC_STANDALONE=1; without this hook the
    cluster came up with no KDC pod and the cross-realm trust principals
    had nowhere to land.

  - knoe/milestone.py (Milestone._get_script_env):
    Clear KUBECTL_CONTEXT in addition to KUBECONTEXT so stale entries
    from a different machine's cfg don't override the kubeconfig's
    own current-context.

  - etc/knoe_cfg.sh (_knoe_read_cfg):
    Skip KUBECTL_CONTEXT / KUBE_CONTEXT_NAME / KUBECONTEXT entries when
    reading cfg in k3s mode. Same theme: kubeconfig current-context is
    authoritative.

  - etc/init_1password.sh + knoe/core/onepassword.py:
    When running non-interactively (no TTY on stdin) and no `op`
    session exists, skip rather than hang on `op signin`. Lets the
    unattended pipeline proceed for k3s/k3d where in-cluster secrets
    are managed separately from 1Password.

Co-authored-by: Junie <junie@jetbrains.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 13:34:10 -07:00
chrisfu
6f99f95f84 kdc: verify install.sh --reset converges to a working cross-realm trust
Fix _deployment_mode_hint() to correctly map cluster_env=service → k3s
mode. Previously 'service' was not handled in the normalized_env checks,
causing fallthrough to build.deploy_env='Dev' → mode='dev' → k3d
dependency required. On a k3s node (myrddin/merlin/gandalf) k3d is not
installed and the DependenciesMilestone fatally aborted the install.

Also fix get_required_dependency_ids(): k3s mode does not require k3d
(k3s is provisioned on the cluster nodes by Ansible, not by the
installer binary).

Fixes: install.sh --mode k3s --reset failing with
  'Dependencies unresolved after install attempts. Required: k3d'

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-11 02:33:38 -07:00
chrisfu
d6586cf1d9 kdc: backport init_kdc.sh trust fixes + add reset-repeatable Junie brief
Mirrors the knoe-db commit `ff7546d` patches into the prole copy of
`etc/init_kdc.sh` so a re-run of `install.sh --mode k3s --reset` from
this repo produces a working cross-realm trust without manual cluster
surgery. The k3s cluster is provisioned from this repo, so the source
fix must live here (knoe-db remains canonical for GKE).

Changes to etc/init_kdc.sh:

1. Create BOTH cross-realm krbtgts in MIT, not just the outbound one.
   The inbound `krbtgt/<REALM>@<TRUST_REALM>` (issued by Samba,
   decrypted here) was missing entirely; without it, MIT cannot
   decrypt inbound TGTs and the trust never carries traffic.

2. Pin both cross-realm krbtgts to RC4 (`arcfour-hmac:normal`). AES
   keys depend on salt, and Samba's `<remote_realm>+UPN` salt does
   not match MIT's `<local_realm>+<principal-no-realm>`; RC4 has no
   salt so both sides converge from the password alone. Matches the
   already-pinned Samba side (commit `ad1eced`).

3. Replace the broken "remote kadmin to Samba" reciprocal-trust block
   with a documented no-op pointing at
   `infrastructure/playbooks/kerberos_trust_setup.yml`. Samba AD
   does not accept additions over MIT's kadmin protocol; the block
   always failed with "Missing parameters in krb5.conf required for
   kadmin client".

4. Switch the KDC data volume from emptyDir to a PVC
   (claimName `knoe-kdc-data`, parameterized by
   `$PROLE_KDC_STORAGE_SIZE` and `$PROLE_KDC_STORAGE_CLASS`).
   State now survives pod restarts.

Adds Junie brief `docs/plans/junie/kdc-trust-reset-repeatable.md`
with four TDD acceptance criteria for an end-to-end --reset run on
the prole k3s cluster.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 01:24:34 -07:00
chrisfu
ad1ecedb5e kerberos_trust_setup: pin trust account to RC4 only (msDS-Supported... 4)
The matching knoe-db change (commit `ff7546d`) re-keys the MIT-side
cross-realm krbtgts with `-e arcfour-hmac:normal` to dodge the
AES-salt mismatch between Samba and MIT. For the trust to actually
carry traffic, the Samba side must also offer only RC4 on the
trust account — otherwise the client picks AES, the two sides
derive different keys from the same password (different salt
conventions), and TGT decryption fails.

Change `msDS-SupportedEncryptionTypes` from 28 (RC4+AES128+AES256)
to 4 (RC4 only). RC4 has no salt, so keys derive from the password
alone and both sides converge.

Updated docstring + summary debug print to reflect the new value
and the why.

Tracked alongside the Junie brief at
~/dev/knoe-db/docs/plans/junie/kdc-trust-reset-repeatable.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 00:29:01 -07:00
chrisfu
4fe8647953 init_kdc.sh: fix realm default from PROLE.LOCAL to KNOE.LOCAL
The post-rebrand realm is KNOE.LOCAL (matches knoe-db/etc/init_kdc.sh
already). The prole copy of init_kdc.sh was never updated, so the k3s
KDC pods on myrddin/merlin/gandalf got initialized with PROLE.LOCAL
realm baked into /etc/krb5.conf and /etc/krb5kdc/kdc.conf.

When chrisfu later wired up the Samba <-> in-cluster KDC cross-realm
trust playbook (kerberos_trust_setup.yml, targeting KNOE.LOCAL), the
trust was bidirectionally correct on the Samba side but the in-cluster
KDC could not decrypt the inbound krbtgt/KNOE.LOCAL@PROLE.ORG TGT
because its own realm was PROLE.LOCAL — different krbtgt key, every
TGS_REQ failed with "Decrypt integrity check failed" (manifested as
"Server not found in Kerberos database" in the client trace).

This commit fixes the source. To pick it up in-cluster, the active
KDC pod must be redeployed (its DB sits on an emptyDir volume so a
restart re-initializes against this default).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:37:31 -07:00
chrisfu
19d1f136de ansible: fix klist principal form in trust-setup summary print
klist shows the Kerberos principal (slash form
krbtgt/KNOE.LOCAL@PROLE.ORG), not the sAMAccountName underscore form
(krbtgt_KNOE.LOCAL). Update the summary debug task accordingly and
split the kvno hint into two stages (the cross-realm TGT first, then
a service ticket) so the user can isolate which step fails when
chasing salt/enctype issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:37:54 -07:00
chrisfu
78eef6fbd9 ansible: hoist ldb-tools install to top of play
The apt install task was positioned after the user-create step, so a
failure in create (or anywhere upstream) meant ldb-tools never got
installed. Move the apt task to the very top of the play, before the
cluster-secret lookups, so it always runs first on every invocation.

Drop the duplicate apt task that previously sat just before the
ldbmodify locator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:35:18 -07:00
chrisfu
4be9fbcafe ansible: idempotency check now uses underscore account name
The _trust_exists probe was still grepping samba-tool user list for
"krbtgt/KNOE.LOCAL" (slash form), but the actual sAMAccountName is
"krbtgt_KNOE.LOCAL" (underscore form, since slash is invalid in
sAMAccountName). _trust_exists was therefore always false, the create
step retried against the existing account on every re-run, and Samba
rejected it as a name collision.

Match against samba_account_name directly so re-runs route through
the setpassword branch instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:32:49 -07:00
chrisfu
52ab023ae2 ansible: install ldb-tools package (provides ldbmodify)
myrddin doesn't have ldbmodify installed — it's in the ldb-tools
Debian package, separate from samba-tool. Add an apt task ahead of
the locator so the package is in place before we try to use it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:20:26 -07:00
chrisfu
c45671283b ansible: locate ldbmodify binary explicitly (not on sudo PATH)
ldbmodify is part of the ldb-tools Debian package but is not in
root's default PATH under sudo on the Samba DC, so the user-attribute
modify step failed with "ldbmodify: command not found".

Add a locator task that probes the conventional install paths
(/usr/bin, /usr/sbin, /usr/local/bin, /opt/samba/bin) and falls back
to a depth-limited find under /usr and /opt. The downstream
ldbmodify invocation now uses the absolute path resolved here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:19:38 -07:00
chrisfu
3485046dbd ansible: temp-disable Samba password complexity for trust account write
samba-tool user create rejected the cluster-generated trust_shared_password
with "0000052D: Constraint violation - check_password_restrictions: the
password does not meet the complexity criteria". The trust password is
high-entropy random bytes from init_kdc.sh and may not satisfy AD's
"3 of 4 character categories" rule.

Wrap the create + setpassword steps in a block that:
  1. reads the current `samba-tool domain passwordsettings show` state
  2. flips complexity off if it was on
  3. does the user write
  4. always: restores complexity to its prior on/off state

The restore is in an `always:` clause so a failure inside the write
block does not leave the domain policy weakened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:09:41 -07:00
chrisfu
aa541af3f5 ansible: use krbtgt_KNOE.LOCAL sAMAccountName + UPN for trust account
samba-tool user create rejected the sAMAccountName "krbtgt/KNOE.LOCAL"
with "samldb: sAMAccountName contains invalid '/' character". AD/Samba
disallow "/" in sAMAccountName even though Kerberos principal names
require it (krbtgt/REMOTE@LOCAL).

Switch storage name to "krbtgt_KNOE.LOCAL" and add an ldbmodify pass
that sets the canonical Kerberos identity on the same object:

  userPrincipalName     = krbtgt/KNOE.LOCAL@PROLE.ORG
  servicePrincipalName  = krbtgt/KNOE.LOCAL
  msDS-SupportedEncryptionTypes = 28  (RC4+AES128+AES256)

Samba KDC resolves principals by UPN/SPN, so a TGS-REQ for
krbtgt/KNOE.LOCAL@PROLE.ORG will hit this account.

Note: key-salt parity with the MIT side is NOT guaranteed yet. Samba's
default salt for AES keys is REALM+UPN; MIT's default for cross-realm
krbtgt is REALM+"krbtgt"+REMOTE. If kvno fails with "decrypt integrity
check failed" we'll add a keytab-export/import step in a follow-up
rather than try to coerce Samba's salt at creation time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 22:02:55 -07:00
chrisfu
6740e3dcc5 ansible: guard krbtgt smoke probe against --check mode failure
In --check mode the user-create step is skipped (it's mutating), so
the subsequent `samba-tool user show krbtgt/KNOE.LOCAL` probe fails
with rc=1 on a first dry-run because the user doesn't yet exist. Add
the same guarded failed_when used on the trust validate/show tasks
earlier: only fail when NOT (check_mode AND trust didn't pre-exist).

Real runs still fail hard on rc!=0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 21:57:51 -07:00
chrisfu
555da4acfc ansible: rewrite kerberos_trust_setup for MIT KDC trust
samba-tool domain trust create --type=external only works against
another writeable AD-style DC; against an MIT KDC it fails with
"Failed to find a writeable DC for domain 'KNOE.LOCAL'". Our peer at
svc/auth in knoe-system is a plain Kerberos KDC, so we cannot use that
path.

Replace the trust create/validate/show steps with the supported
Samba-AD <-> MIT-KDC approach: create the inter-realm TGT principal as
an ordinary user account in Samba whose sAMAccountName is
"krbtgt/KNOE.LOCAL", with its password synced to the cluster Secret
knoe-system/knoe-kdc-secrets/trust_shared_password (which the MIT side
already keys against). Steps:

  1. samba-tool user list                            (idempotency probe)
  2. samba-tool user create krbtgt/KNOE.LOCAL ...    (when missing)
  3. samba-tool user setpassword krbtgt/KNOE.LOCAL   (when present, to
     re-sync after a cluster Secret rotation)
  4. samba-tool user setexpiry --noexpiry
  5. ldbmodify msDS-SupportedEncryptionTypes=28      (RC4+AES128+AES256
     to match what init_kdc.sh sets on the MIT side)
  6. samba-tool user show                            (smoke probe)
  7. debug task prints the manual kvno smoke-test command

Cluster-secret lookups and krb5.conf [realms]/[domain_realm] edits are
unchanged - those parts were correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:54:24 -07:00
chrisfu
227f49042c ansible: opt read-only kubectl/samba-tool tasks into --check mode
Ansible --check mode auto-skips command/shell tasks unless they
explicitly opt in via check_mode:false. The two cluster-state lookups
(svc/auth ClusterIP and knoe-kdc-secrets/trust_shared_password) were
silently skipped during dry-run, leaving trust_kdc_ip empty and tripping
the assertion immediately.

Add check_mode:false to the five read-only tasks so a --check run can
still resolve cluster state and report what would change:
  - Resolve trust_kdc_ip from cluster
  - Resolve trust_shared_password from knoe-kdc-secrets
  - samba-tool domain trust list  (idempotency probe)
  - samba-tool domain trust validate
  - samba-tool domain trust show  (smoke print)

The mutating create step (samba-tool domain trust create) keeps the
default behavior - skipped in check mode. validate/show gain a guarded
failed_when so a check-mode dry-run on a host where the trust does not
yet exist does not fail (the create was skipped, so a non-zero rc is
expected). Real runs still fail hard on rc!=0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 01:51:30 -07:00
chrisfu
5cece408bd ansible: switch kerberos_trust_setup to KNOE.LOCAL + vault wiring
The cross-realm trust playbook was written for the pre-rebrand realm name
PROLE.LOCAL, which no longer exists in the in-cluster KDC — the canonical
realm is now KNOE.LOCAL. The Samba AD trust was therefore never actually
established, leaving chrisfu@KNOE.LOCAL→chrisfu@PROLE.ORG service ticket
flows blocked (and blocking pg_oauth / db.prole.org Kerberos work).

Changes:
- Realm: PROLE.LOCAL → KNOE.LOCAL across all task names, vars, and the
  krb5.conf [realms] / [domain_realm] blocks added on myrddin.
- samba_admin_password now resolves from the Ansible vault var
  vault_samba_dns_admin_pass (group_vars/ad_dc/vault.yml) by default,
  with SAMBA_ADMIN_PASSWORD env and -e overrides preserved for CI.
- trust_shared_password auto-resolves from the in-cluster Secret
  knoe-system/knoe-kdc-secrets (key trust_shared_password) when not
  passed explicitly — same Secret init_kdc.sh writes.
- Added [domain_realm] mapping for *.knoe.local → KNOE.LOCAL so Samba
  can resolve in-cluster service principals.
- Added a final `samba-tool domain trust show` smoke step so a successful
  run prints the visible trust state for log review.

Run:
  ANSIBLE_VAULT_PASSWORD_FILE=$PWD/.vault_pass   ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:26:31 -07:00
chrisfu
48def19637 fix(inventory): specify ssh key for zinfandel — non-default key name 2026-05-09 22:42:26 -07:00
chrisfu
9020eae91e fix(ansible): remove become from Windows play — connect as admin directly
win_regedit writes HKLM keys without privilege escalation when
ansible_user is already a local/domain Administrator. become:true
with --ask-become-pass was overriding the playbook's runas method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:36:48 -07:00
chrisfu
67b483bcf2 fix(ansible): split workstation playbook into two plays for Unix vs Windows
A single play with become:true uses sudo by default, which Ansible's
Windows exec wrapper rejects immediately at gather_facts. Split into:
- Play 1: hosts workstations,!workstations_windows — macOS/Linux, sudo
- Play 2: hosts workstations_windows — Windows, become_method:runas

Also set morgana to ansible_connection=local (running Ansible from
morgana itself, so SSH self-lookup fails DNS resolution).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:33:51 -07:00
chrisfu
5cd22c53d9 feat(inventory): add all 4 workstations; add Windows SPNEGO policy support
Add morgana and zinfandel (macOS) and morgoth + fairyland (Windows)
to the workstations inventory.

Extend workstation_kerberos.yml to handle Windows targets:
- Chrome and Edge AuthServerAllowlist set via win_regedit under
  HKLM\SOFTWARE\Policies\{Google\Chrome,Microsoft\Edge}
- krb5.conf and plist tasks skip on Windows (uses built-in SSPI/AD)
- Windows hosts in [workstations_windows] group with WinRM/NTLM
  transport; switch to kerberos transport after domain join

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:21:21 -07:00
chrisfu
d5f6e8f6a9 fix(gitea): set recovery password after admin promotion, not before
Gitea's AdminEditUser PATCH with source_id:0 resets the password field
as a side effect when applied to a SPNEGO-registered user (the auth
record re-initialisation clears the local password). Setting the
password before the promotion PATCH meant it was immediately wiped.

Move the 1Password recovery password step to run after the admin
promotion PATCH so the final Gitea state matches 1Password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:15:50 -07:00
chrisfu
5d545d238c fix(gitea): include email in admin API set-password PATCH call
Gitea's AdminEditUser endpoint requires email alongside login_name
and source_id. Without it the server returns 200 but silently
ignores the password field — causing login failures despite the
script reporting success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:05:29 -07:00
chrisfu
7b782afef4 chore: restore vault secrets added on myrddin 2026-05-09 21:48:54 -07:00
chrisfu
cad75996b5 fix: set recovery password unconditionally after any admin token path
The 1Password password-setting block was nested inside path 2 of
promote_gitea_admin(). When path 1 found a cached gitea-admin-token
k8s secret, path 2 was skipped entirely and the recovery password
was never set in Gitea, leaving the user unable to log in.

Move the password-setting step to run unconditionally after all four
token paths complete. Uses _op_ensure_auth() so it degrades gracefully
in CI/headless environments where 1Password is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 21:33:15 -07:00
chrisfu
7b391ab5ed feat(onboarding): scalable Kerberos SSO onboarding — service account auth, Ansible workstation policy
Problem: 'defaults write' is not an onboarding path, it's a support
incident waiting to happen. Browser SPNEGO must be deployed via managed
policy at machine provisioning time, not per-user.

Changes:
  etc/init_knoe_users.sh
  - _op_ensure_auth(): tries OP_SERVICE_ACCOUNT_TOKEN env, k8s secret
    'op-service-account-token', existing interactive session, and op-run
    inheritance — in that order. Gives actionable instructions when none
    work, including the correct 'op run --' invocation documented in k3s.cfg.
  - gitea_ensure_password(): calls _op_ensure_auth() at entry; eliminates
    the silent 'op not authenticated' failure path.
  - Script header: documents 'op run -- bash etc/init_knoe_users.sh' as
    the intended invocation for admin's laptop.
  - 'Next steps' output: replaces 'defaults write' with reference to
    workstation_kerberos.yml Ansible playbook.

  infrastructure/playbooks/workstation_kerberos.yml (new)
  - Deploys /etc/krb5.conf (PROLE.ORG realm, myrddin KDC) to all managed
    endpoints.
  - Deploys Chrome + Edge managed policy (AuthServerAllowlist) on macOS
    and Linux — no per-user browser configuration ever required.
  - Idempotent; run during laptop provisioning or re-run at any time.
  - Targets 'workstations' Ansible group.

  infrastructure/inventory/hosts.ini
  - Adds [workstations] group with example entries and onboarding notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:10:32 -07:00
chrisfu
acfdee1fdc fix(gitea): replace UUID placeholder email with configured address on admin promotion
SPNEGO auto-registration assigns a UUID@localhost placeholder email.
promote_gitea_admin() now detects the UUID pattern and replaces it with
the user's real configured email (KNOE_ADMIN_EMAIL / GITEA_ADMIN_EMAIL).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:40:54 -07:00
chrisfu
0877fe5190 fix(gitea): downgrade 1Password failures to WARN; add signin hint
1Password credentials are optional (primary auth is Kerberos SPNEGO).
When op is not signed in on a headless server, show a WARN with a
helpful hint instead of an ERROR that suggests a hard failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:37:00 -07:00
chrisfu
cddd9c8889 fix(gitea): redirect log() calls to stderr in value-returning functions
log() writes to stdout, so log calls inside functions that return
values via printf/stdout contaminate the captured output.

gitea_helm_admin_token and gitea_ensure_password now redirect all
log() calls to stderr with >&2, keeping stdout clean for the
returned token/password string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:34:58 -07:00
chrisfu
23e87f074c fix(gitea): include email in PATCH body; fix HTTP status parsing; add diagnostics
Gitea admin edit-user API requires email in PATCH body.
Fix HTTP status parser (tr -d vs broken cut -d_).
Add logging to user-existence check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:31:34 -07:00
chrisfu
a2aaea8bda fix(gitea): add required scopes to API token creation (Gitea ≥1.22)
Gitea 1.22 requires explicit scopes in the token creation POST body.
Without them the API returns: "access token must have a scope".

Now requests: read/write:admin, read/write:user, read/write:issue,
read/write:repository — sufficient for admin user management.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:28:58 -07:00
chrisfu
f8c948b2ab fix(gitea): read admin password from running pod's RS, add API diagnostic logs
When a Deployment has been updated but the new pod keeps crashing before
configure-gitea runs (e.g. after a failed Helm upgrade), the running pod
still carries the old RS's password — which differs from the current
Deployment spec.

gitea_helm_admin_token() now walks: running-pod-RS → current-Deployment-spec,
trying each password until one produces a valid API token.

Also adds HTTP status + response body to gitea_api_token() failures so the
cause is visible without exposing credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:27:11 -07:00
chrisfu
6de7218ec7 fix(gitea): read Helm admin credentials from Deployment env vars, not secret
The Gitea Helm chart (gitea-12.5.3) stores GITEA_ADMIN_USERNAME and
GITEA_ADMIN_PASSWORD as plain values in the configure-gitea init container
spec — not in a k8s Secret with key 'admin-password'.  The previous code
looked for a non-existent secretKeyRef and returned empty, causing the
REST API bootstrap path to fail.

Now reads credentials via:
  kubectl get deployment gitea ... env[?(@.name=="GITEA_ADMIN_PASSWORD")].value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:14:21 -07:00
chrisfu
5fba20b651 fix(gitea): correct Helm admin secret name to 'gitea' (not 'gitea-gitea')
The Gitea Helm chart names the admin credentials secret after the release
name alone, not '{release}-gitea'. For release name 'gitea' the secret is
just 'gitea'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:06:26 -07:00
chrisfu
dbe249ce9f fix(gitea): REST API bootstrap path bypasses broken admin CLI; fix DB namespace resolver
deploy.sh: resolve_knoe_db_namespace() now checks DATABASE_NAMESPACE from
config (k3s.cfg: knoe-db) before probing namespaces. Previously it fell back
to 'default' because only 'knoe' was probed, baking a wrong DB hostname into
app.ini. The running server was unaffected (uses GITEA__database__HOST env var)
but the gitea admin CLI, which reads app.ini directly, could not connect.

init_knoe_users.sh: add gitea_helm_admin_token() which reads the Helm
bootstrap admin password from the gitea-gitea k8s secret and exchanges it for
a REST API token — entirely avoiding the broken CLI. Add gitea_api_set_password()
which uses PATCH /api/v1/admin/users/{user} to set the password via the running
web server (which has the correct DB connection) instead of kubectl exec.

promote_gitea_admin() now prefers the REST-API-only path:
  1. existing token (env / k8s secret)
  2. Helm admin k8s secret → REST token; set KNOE_ADMIN_PRINCIPAL password via API
  3. 1Password → CLI set-password → basic-auth token (fallback)
  4. kubectl exec generate-access-token (last resort)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:53:55 -07:00
chrisfu
abb38fc1e9 fix(cfg): pre-scan --mode before sourcing knoe_cfg.sh so k3s.cfg is loaded
knoe_cfg.sh selects the config file based on KNOE_MODE at source time.
Without pre-scanning argv, KNOE_MODE is empty and k3d.cfg wins the
fallback loop even when --mode k3s is passed.  This caused k3s.cfg values
(e.g. KNOE_ADMIN_PRINCIPAL=chrisfu) to be silently ignored, defaulting
to 'admin' and causing gitea_set_password to fail against a non-existent user.

Also surface gitea_set_password errors (without logging the password) so
failures are diagnosable rather than silent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:40:04 -07:00
chrisfu
58496a31e4 feat(gitea): 1Password-backed credential management; never log plaintext passwords
Replace the predictable temp-password pattern with a secure credential flow:

- gitea_ensure_password(): creates a 1Password item with --generate-password
  (1P generator, 32-char letters+digits) if one does not exist; retrieves it
  on re-runs (idempotent). Password never appears in logs or env vars.

- gitea_set_password(): sets the Gitea account password via kubectl exec
  (gitea admin CLI, no prior Gitea auth required).

- gitea_api_token(): exchanges basic-auth credentials for a REST API token,
  deleting any stale token of the same name first. Token only; password
  cleared from memory immediately after use.

- promote_gitea_admin(): tries credential sources in order:
  1. GITEA_ADMIN_TOKEN env / gitea-admin-token k8s secret
  2. 1Password (generates if missing) → set password → REST token
  3. kubectl exec generate-access-token fallback (Helm bootstrap admin)
  Tokens persisted in k8s secret (revocable); passwords only in 1Password.

The Gitea password is a vault credential for emergency recovery only.
Normal auth is always Kerberos SPNEGO — users never need to type a password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:07:47 -07:00
chrisfu
abb060d614 fix(users): remove fictitious ron@prole.org email; show Kerberos identity in provisioning log
Ron has no Google Workspace prole.org account. Email is now NULL in the DB
(NULLIF('')). The provision_user log line now always shows the Kerberos
principal explicitly and labels the email field separately so it is clear
which identity type is in use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:03:59 -07:00
chrisfu
57cd93ae00 fix(init_kdc): prevent stale k3d LOCAL_REGISTRY_INTERNAL from leaking into init_kdc.sh
In k3s mode, LOCAL_REGISTRY_INTERNAL set to a k3d value in the shell environment
leaked into init_kdc.sh subprocesses, causing pods to use the wrong image address
while the push went to myrddin.prole.org:5000.

Add _resolve_kdc_registry() helper that derives correct registry values from
KNOE_IMAGE_REGISTRY and PROLE_K3S_SERVER. Both init_kdc.sh call sites now pass
explicit overrides to prevent any inherited k3d value from leaking through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:53:57 -07:00
chrisfu
385f74b4f7 fix(init_knoe_users): replace multiline die with err+exit to fix pipe syntax error
The 'die' call with a multiline string containing '|' (for base64 pipeline
in the help text) was parsed by bash as a pipe operator. Replace with
individual err() calls followed by explicit exit 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:33:58 -07:00
chrisfu
991c04d936 feat(init_knoe_users): 1Password fallback for KDC passwords; try prole-kdc-secrets
Password resolution order for PROLE_KDC_MASTER_PASSWORD and KNOE_GUEST_PASSWORD:
  1. Env var (existing)
  2. knoe-kdc-secrets k8s Secret (existing)
  3. prole-kdc-secrets k8s Secret (new — used by live prole deployment)
  4. 1Password via op read (new — OP_KDC_MASTER_PASSWORD_REF / OP_KDC_GUEST_PASSWORD_REF)
  5. PROLE_LOCAL_ADMIN_PASSWORD (existing)
  6. die with actionable message

Add try_op_read() helper: wraps op CLI, returns 1 if op not available or ref empty.

conf/k3s.cfg: add KNOE_ADMIN_PRINCIPAL=chrisfu, OP_KDC_MASTER_PASSWORD_REF,
  OP_KDC_GUEST_PASSWORD_REF placeholders (empty — user fills in op:// URIs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:15:02 -07:00
chrisfu
2f8b88a97a fix(init_knoe_users): allow 'default' kubectl context in k3s/k8s mode
The k3s-generated kubeconfig on the cluster server node uses 'default'
as its context name. The existing guard rejected this unconditionally,
causing init_knoe_users.sh to always fail when run from myrddin.

Tighten the guard: only reject 'default' when KNOE_MODE is k3d (where
it would indicate the k3d context was not found). In k3s/k8s mode,
'default' is legitimate and the script proceeds normally.

Usage on myrddin: bash etc/init_knoe_users.sh --mode k3s initialize

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:44:02 -07:00
chrisfu
3d92781ae6 feat(init_knoe_users): add Gitea SPNEGO keytab step; fix PROLE.LOCAL→KNOE.LOCAL
- Add ensure_gitea_spnego_keytab(): provisions HTTP/git.prole.org@PROLE.ORG
  service principal, keytab, and gitea-krb5-keytab Secret automatically when
  samba-tool is available (i.e. running on myrddin); otherwise prints manual
  steps. Restarts gitea-spnego-proxy after keytab is created.
- Add GITEA_SPNEGO_HOST, GITEA_KRB5_AD_REALM, GITEA_KRB5_AD_USER config vars.
- Fix promote_gitea_admin: exec with 'su git' (gitea refuses to run as root);
  use KNOE_ADMIN_PRINCIPAL instead of hardcoded 'admin'; fall back to
  'gitea_admin' (Helm chart default) if the primary user has no token yet.
- Rename initialize() step numbers (9=ArgoCD, 10=SPNEGO keytab, 11=Gitea, 12=GitLab).
- Fix PROLE.LOCAL → KNOE.LOCAL in: default realm, SQL schema default, comments,
  next-steps message, trust principal comments.
- show_status: add gitea-krb5-keytab check alongside pg keytab check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:31:16 -07:00
chrisfu
1cc9c4e07a fix(gitea): pin image registry to docker.io, disable rootless variant
Gitea Helm chart v10.x changed defaults:
  image.registry: docker.gitea.com  (was docker.io)
  image.rootless: true              (was false)

Our values set repository/tag but not registry/rootless, so the new
defaults produced docker.gitea.com/gitea/gitea:1.22.3-rootless which
can't be pulled from gandalf. Pin registry: docker.io and rootless: false
to keep using the standard Docker Hub image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 23:18:16 -07:00
chrisfu
363123ca8e fix(gitea): disable valkey-cluster; pin storageClass in Helm values
Helm upgrade was failing for two reasons:
1. persistence.storageClass was unset (rendered null), conflicting with the
   existing PVC's immutable gitea-local-d005 storageClassName.
2. valkey-cluster (and redis-cluster) were not explicitly disabled, causing
   the latest gitea chart to spin up a valkey StatefulSet on each upgrade.

Add GITEA_STORAGE_CLASS env-var override for non-k3s modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:04:26 -07:00
chrisfu
5b30c2c5b2 fix(spnego-proxy): add statusCapture to log SPNEGO auth failures
gokrb5 returns 401 silently on invalid tickets. Wrap ResponseWriter to
capture status code and log failures with remote addr, host, path, and
first 20 chars of the Negotiate token for easier debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:43:20 -07:00
chrisfu
5cfd24cd05 fix(auth): fix gitea krb5.conf KDC hostname: knoe-auth→auth
KDC Service is 'auth' (ports 88/749); 'knoe-auth' only exposes HTTP port 8080.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:18:22 -07:00