feat(gitea): SPNEGO Kerberos SSO for git.prole.org

Three-part fix to make browser and curl SPNEGO auth work end-to-end:

1. spnego-proxy: issue WWW-Authenticate: Negotiate challenge when no
   Authorization header is present so Chrome (with AuthServerAllowlist)
   and curl --negotiate automatically present Kerberos tokens. Previously
   the proxy only validated tokens if the client proactively sent them.
   Pass-through preserved for non-Negotiate schemes (Basic/token) so
   git CLI users with PATs continue to work via Gitea own auth.

2. gitea_spnego_keytab.yml: new Ansible playbook that provisions the
   gitea-http AD account (AES-only, msDS-SupportedEncryptionTypes=24),
   registers SPN HTTP/git.prole.org, resets the password to derive fresh
   AES keys, exports the domain keytab, and rekeys it to principal name
   HTTP/git.prole.org@PROLE.ORG that gokrb5 needs for keytab lookup.
   Key lesson: samba-tool exportkeytab --principal=HTTP/... returns empty;
   must export full domain keytab and rekey in Python.

3. init_gitea.sh: add setup_gitea_spnego() calling the Ansible playbook
   in k3s mode as part of the standard deploy flow, with inline notes
   on every non-obvious constraint discovered during this work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chrisfu 2026-05-28 05:19:59 -07:00
parent b245593b0c
commit 5077e1335d
3 changed files with 644 additions and 4 deletions

View File

@ -463,6 +463,73 @@ spec:
EOF
}
setup_gitea_spnego() {
# Provision the SPNEGO keytab and deploy the gitea-spnego-proxy sidecar.
#
# In k3s mode: runs the Ansible playbook gitea_spnego_keytab.yml which:
# - Creates/updates the gitea-http AD account with AES-only encryption
# - Registers the SPN HTTP/git.prole.org on that account
# - Exports and rekeys the keytab to HTTP/git.prole.org@PROLE.ORG
# - Applies the k8s secret gitea-krb5-keytab in the gitea namespace
# - Restarts the gitea-spnego-proxy deployment
#
# Key knowledge captured here:
# - samba-tool domain exportkeytab with --principal=HTTP/git.prole.org
# produces an empty keytab. The account principal (gitea-http@PROLE.ORG)
# must be exported via the full domain keytab and rekeyed in Python.
# - msDS-SupportedEncryptionTypes=24 must be set before the password reset
# to ensure the KDC generates AES keys (16=AES256, 8=AES128, sum=24).
# - gokrb5 v8 matches keytab entries by principal name components, so the
# keytab MUST contain HTTP/git.prole.org@PROLE.ORG (not gitea-http@PROLE.ORG)
# for the SPNEGO token decryption to succeed.
# - The proxy must issue WWW-Authenticate: Negotiate on requests with no
# Authorization header; without the challenge Chrome never sends a token.
# - Nodes are arm64 — build the proxy image on myrddin, not cross-compiled.
#
# Keytab rotation (e.g. after AD password policy change):
# ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml \
# -e force_keytab_reset=true
# Apply the spnego-proxy manifest (idempotent — already defined in the k3s
# manifests directory; this ensures it's present after a fresh cluster build).
local spnego_manifest="${SCRIPT_DIR}/../deploy/opentofu/k3s/manifests/knoe/gitea-spnego-proxy.yaml"
if [[ -f "$spnego_manifest" ]]; then
log "Applying gitea-spnego-proxy manifest..."
kubectl apply -f "$spnego_manifest" >/dev/null 2>&1 || warn "gitea-spnego-proxy manifest apply failed (may already be up-to-date)"
else
warn "gitea-spnego-proxy manifest not found at $spnego_manifest — skipping"
fi
if [[ "$MODE" != "k3s" ]]; then
warn "SPNEGO keytab provisioning via Ansible only runs in k3s mode (current: $MODE)."
warn "For other modes, manually run:"
warn " ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml"
return 0
fi
if ! command -v ansible-playbook >/dev/null 2>&1; then
warn "ansible-playbook not found — skipping SPNEGO keytab provisioning."
warn "Run manually: ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml"
return 0
fi
local playbook="${SCRIPT_DIR}/../infrastructure/playbooks/gitea_spnego_keytab.yml"
if [[ ! -f "$playbook" ]]; then
warn "Playbook not found at $playbook — skipping SPNEGO keytab provisioning"
return 0
fi
log "Running SPNEGO keytab provisioning via Ansible..."
if ansible-playbook "$playbook" \
-e "gitea_kube_context=${KUBECONTEXT:-prole-service-cluster}" \
-e "gitea_spnego_namespace=${NAMESPACE}"; then
log "✓ Gitea SPNEGO keytab provisioned — SPNEGO challenge active on git.prole.org"
else
warn "SPNEGO keytab provisioning failed. Gitea will still work via its own login page."
warn "Retry: ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml -e force_keytab_reset=true"
fi
}
ACTION="${1:-deploy}"
case "$ACTION" in
deploy|"") ;;
@ -484,5 +551,9 @@ fi
log "Waiting for deployment rollout..."
kubectl -n "$NAMESPACE" rollout status deploy/gitea --timeout=10m
# Provision the SPNEGO proxy keytab and verify the Negotiate challenge is live.
# This step is idempotent — safe to run on upgrades/re-deploys.
setup_gitea_spnego
log "Done. Service endpoints:"
kubectl -n "$NAMESPACE" get svc gitea-http gitea-ssh

View File

@ -75,7 +75,8 @@ func main() {
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Header.Get("Authorization"), "Negotiate ") {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Negotiate ") {
// Client is presenting a Kerberos ticket — run full SPNEGO negotiation.
// On success: X-WEBAUTH-USER is set by authedInner, Gitea auto-logs in.
// On failure: gokrb5 returns 401 (invalid ticket — correct to reject).
@ -84,12 +85,25 @@ func main() {
if rw.status == http.StatusUnauthorized {
log.Printf("SPNEGO auth FAILED: from=%s host=%s path=%s token-prefix=%.20s",
r.RemoteAddr, r.Host, r.URL.Path,
r.Header.Get("Authorization")[len("Negotiate "):])
auth[len("Negotiate "):])
}
return
}
// No Kerberos ticket — pass through so Gitea can show its own login page.
if auth != "" {
// Other auth scheme (Basic, token) — pass through to Gitea.
// This allows git CLI users with personal access tokens or passwords to
// authenticate directly via Gitea's own credential validation.
log.Printf("pass-through auth: from=%s scheme=%.6s...", r.RemoteAddr, auth)
proxy.ServeHTTP(w, r)
return
}
// No auth header — issue a SPNEGO challenge.
// Browsers with Chrome SPNEGO policy (AuthServerAllowlist=*.prole.org) will
// automatically respond with Authorization: Negotiate <kerberos-token>.
// git CLI users with PATs will retry with Basic auth (handled above).
w.Header().Set("WWW-Authenticate", "Negotiate")
http.Error(w, "Authentication required (SPNEGO/Kerberos)", http.StatusUnauthorized)
log.Printf("SPNEGO challenge issued: from=%s host=%s path=%s", r.RemoteAddr, r.Host, r.URL.Path)
})
log.Printf("spnego-proxy listening on %s, upstream %s", listenAddr, upstreamURL)

View File

@ -0,0 +1,555 @@
---
# gitea_spnego_keytab.yml — Provision the Gitea HTTP SPNEGO keytab from Samba AD
# and deploy it as a Kubernetes secret in the gitea namespace.
#
# What this playbook does:
# 1. Creates (if absent) the gitea-http service account in Samba AD.
# 2. Registers the SPN HTTP/git.prole.org on that account.
# 3. Pins msDS-SupportedEncryptionTypes to AES128+AES256 (value 24, no RC4).
# 4. Resets the account password to derive fresh AES keying material.
# 5. Exports the domain keytab, rekeys it to a per-SPN keytab, and verifies.
# 6. Fetches the keytab to the control node.
# 7. Applies it as the Kubernetes secret gitea-krb5-keytab in the gitea namespace.
# 8. Restarts and waits for the gitea-spnego-proxy deployment.
# 9. Smoke-tests that git.prole.org returns an HTTP 401 (SPNEGO challenge).
#
# PREREQUISITES
# -------------
# 1. myrddin.prole.org is reachable and is the Samba AD DC for PROLE.ORG.
# 2. python3 is available on myrddin (used for keytab rekey step).
# 3. kubectl on the control node is configured with context prole-service-cluster
# and has permission to create/update secrets in namespace gitea.
# 4. The gitea-spnego-proxy deployment exists in the gitea namespace.
#
# FULL RUN (first time or after AD account changes):
# ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml
#
# Force keytab rotation (e.g. after password policy change):
# ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml \
# -e force_keytab_reset=true
#
# Dry-run:
# ansible-playbook infrastructure/playbooks/gitea_spnego_keytab.yml --check
# ===========================================================================
# Play 1 — Samba AD setup: create account, register SPN, export keytab
# ===========================================================================
- name: Provision gitea-http SPNEGO account and keytab in Samba AD
hosts: ad_dc
gather_facts: false
become: true
vars:
gitea_spnego_account: gitea-http
gitea_spnego_spn: HTTP/git.prole.org
gitea_spnego_realm: PROLE.ORG
gitea_spnego_namespace: gitea
gitea_kube_context: prole-service-cluster
force_keytab_reset: false
gitea_spnego_keytab_local: /tmp/git-http-fetched.keytab
tasks:
# ------------------------------------------------------------------
# 0. Ensure required Debian packages are present
# (ldb-tools provides ldbmodify; used to set msDS-SupportedEncryptionTypes)
# ------------------------------------------------------------------
- name: Ensure ldb-tools is installed (provides ldbmodify)
ansible.builtin.apt:
name: ldb-tools
state: present
update_cache: true
cache_valid_time: 3600
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 1. Idempotency probe — does the gitea-http service account exist?
# ------------------------------------------------------------------
- name: Check whether {{ gitea_spnego_account }} user already exists in Samba
ansible.builtin.command:
cmd: samba-tool user list
register: _samba_users
changed_when: false
failed_when: false
check_mode: false
tags: [spnego, keytab, gitea]
- name: Set fact — gitea-http account already present
ansible.builtin.set_fact:
gitea_account_is_new: "{{ gitea_spnego_account not in _samba_users.stdout }}"
tags: [spnego, keytab, gitea]
- name: Report account pre-existence
ansible.builtin.debug:
msg: >-
{{ gitea_spnego_account }}
{{ 'is missing — will create.' if gitea_account_is_new
else 'already present in Samba.' }}
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 2. Create gitea-http if missing
# ------------------------------------------------------------------
- name: Create {{ gitea_spnego_account }} service account if missing
ansible.builtin.command:
argv:
- samba-tool
- user
- create
- "{{ gitea_spnego_account }}"
- "--random-password"
- "--description=Gitea HTTP SPNEGO service account"
register: _gitea_create
changed_when: _gitea_create.rc == 0
failed_when: _gitea_create.rc != 0
when: gitea_account_is_new | bool
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 3. Ensure SPN HTTP/git.prole.org is registered on the account
#
# samba-tool spn add exits 0 whether or not the SPN was already
# present but emits "already has SPN" on stdout when it's a no-op.
# We mark changed_when=false and suppress the "already exists"
# non-error so reruns stay clean.
# ------------------------------------------------------------------
- name: Ensure SPN {{ gitea_spnego_spn }} is registered on {{ gitea_spnego_account }}
ansible.builtin.command:
argv:
- samba-tool
- spn
- add
- "{{ gitea_spnego_spn }}"
- "{{ gitea_spnego_account }}"
register: _gitea_spn
changed_when: false
failed_when:
- _gitea_spn.rc != 0
- "'already' not in (_gitea_spn.stderr | default('') | lower)"
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 4. Locate ldbmodify binary
# (same pattern as kerberos_trust_setup.yml)
# ------------------------------------------------------------------
- name: Locate ldbmodify binary
ansible.builtin.shell:
cmd: |
for p in /usr/bin/ldbmodify /usr/sbin/ldbmodify /usr/local/bin/ldbmodify /opt/samba/bin/ldbmodify; do
[ -x "$p" ] && { echo "$p"; exit 0; }
done
# Fallback: filesystem scan limited to common roots
found=$(find /usr /opt -maxdepth 4 -type f -name ldbmodify 2>/dev/null | head -1)
[ -n "$found" ] && { echo "$found"; exit 0; }
echo "ldbmodify not found" >&2
exit 1
register: _ldbmodify_path
changed_when: false
check_mode: false
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 5. Set msDS-SupportedEncryptionTypes=24 (AES128+AES256, no RC4)
#
# Value 24 = 0x18 = bit3 (AES128-CTS-HMAC-SHA1-96) |
# bit4 (AES256-CTS-HMAC-SHA1-96).
# Explicitly excludes RC4 (bit2=4) so the Samba KDC will only
# issue AES-keyed service tickets for this principal.
# Unlike the cross-realm krbtgt (which must use RC4 for MIT
# interop), HTTP SPNEGO service tickets are local to PROLE.ORG
# and can safely use AES end-to-end.
# ------------------------------------------------------------------
- name: Set msDS-SupportedEncryptionTypes=24 (AES128+AES256) on {{ gitea_spnego_account }}
ansible.builtin.shell:
cmd: |
set -euo pipefail
ldif=$(mktemp)
cat > "$ldif" <<'EOF'
dn: CN=gitea-http,CN=Users,DC=prole,DC=org
changetype: modify
replace: msDS-SupportedEncryptionTypes
msDS-SupportedEncryptionTypes: 24
EOF
"{{ _ldbmodify_path.stdout | trim }}" -H /var/lib/samba/private/sam.ldb "$ldif"
rm -f "$ldif"
register: _gitea_enctype
changed_when: "'Modified 1 records' in (_gitea_enctype.stdout | default(''))"
failed_when: _gitea_enctype.rc != 0
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 6. Generate a strong random password for AES key derivation,
# then reset the account password.
#
# Gated on: account is new OR caller passed -e force_keytab_reset=true.
# The password is ephemeral — we only need it to derive AES keys
# in the keytab. Samba generates the keys at setpassword time.
# no_log protects the plaintext password from appearing in output.
# ------------------------------------------------------------------
- name: Generate ephemeral password for AES key derivation
ansible.builtin.set_fact:
_gitea_spnego_password: "{{ lookup('password', '/dev/null length=40 chars=ascii_letters,digits') }}"
no_log: true
when: gitea_account_is_new | bool or force_keytab_reset | bool
tags: [spnego, keytab, gitea]
- name: Reset {{ gitea_spnego_account }} password to derive fresh AES keys
ansible.builtin.command:
argv:
- samba-tool
- user
- setpassword
- "{{ gitea_spnego_account }}"
- "--newpassword={{ _gitea_spnego_password }}"
no_log: true
register: _gitea_setpw
changed_when: _gitea_setpw.rc == 0
failed_when: _gitea_setpw.rc != 0
when: gitea_account_is_new | bool or force_keytab_reset | bool
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 7. Export the full domain keytab to /tmp/full-domain.keytab
#
# We export all accounts (no --principal filter) and then rekey
# to a per-SPN keytab in step 8. This avoids having to construct
# the exact principal name expected by samba-tool's filter flag.
# ------------------------------------------------------------------
- name: Export full domain keytab to /tmp/full-domain.keytab
ansible.builtin.command:
argv:
- samba-tool
- domain
- exportkeytab
- /tmp/full-domain.keytab
register: _gitea_export
changed_when: _gitea_export.rc == 0
failed_when: _gitea_export.rc != 0
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 8. Rekey full-domain.keytab → git-http.keytab
#
# The inline Python script:
# - Parses the MIT keytab format (magic 0x0502, big-endian).
# - Finds all entries for gitea-http@PROLE.ORG with etype 17
# (AES128) or 18 (AES256) at the highest KVNO.
# - Rewrites those entries with the principal name changed to
# HTTP/git.prole.org@PROLE.ORG (name_type=3, KRB_NT_SRV_HST).
# - Writes the result to /tmp/git-http.keytab.
#
# This is the format required by Apache mod_auth_gssapi, nginx
# SPNEGO modules, and the gitea-spnego-proxy sidecar.
# ------------------------------------------------------------------
- name: Rekey domain keytab to SPN-named keytab /tmp/git-http.keytab
ansible.builtin.command:
argv:
- python3
- -c
- |
import struct, sys
ETYPE_AES128, ETYPE_AES256 = 17, 18
TARGET_PRINCIPAL_COMPONENTS = [b"gitea-http"]
TARGET_REALM = b"PROLE.ORG"
OUTPUT_COMPONENTS = [b"HTTP", b"git.prole.org"]
OUTPUT_NAME_TYPE = 3 # KRB_NT_SRV_HST
OUTPUT_REALM = b"PROLE.ORG"
INPUT_PATH = "/tmp/full-domain.keytab"
OUTPUT_PATH = "/tmp/git-http.keytab"
def read_u8(data, pos):
return data[pos], pos + 1
def read_u16be(data, pos):
return struct.unpack_from(">H", data, pos)[0], pos + 2
def read_u32be(data, pos):
return struct.unpack_from(">I", data, pos)[0], pos + 4
def read_counted(data, pos):
length, pos = read_u16be(data, pos)
value = data[pos:pos + length]
return value, pos + length
def read_entry(data, pos):
"""Read one keytab entry, return (entry_dict, next_pos)."""
size, pos = read_u32be(data, pos)
if size == 0:
return None, pos
entry_start = pos
entry_end = pos + size
num_components, pos = read_u16be(data, pos)
realm, pos = read_counted(data, pos)
components = []
for _ in range(num_components):
comp, pos = read_counted(data, pos)
components.append(comp)
name_type, pos = read_u32be(data, pos)
timestamp, pos = read_u32be(data, pos)
kvno8, pos = read_u8(data, pos)
etype, pos = read_u16be(data, pos)
key, pos = read_counted(data, pos)
kvno32 = kvno8
if pos < entry_end:
kvno32, pos = read_u32be(data, pos)
return {
"realm": realm,
"components": components,
"name_type": name_type,
"timestamp": timestamp,
"kvno": kvno32,
"etype": etype,
"key": key,
}, entry_end
def pack_entry(entry):
"""Serialise one keytab entry back to bytes (including 4-byte length prefix)."""
buf = bytearray()
def w16(v): buf.extend(struct.pack(">H", v))
def w32(v): buf.extend(struct.pack(">I", v))
def w8(v): buf.append(v)
def wc(b):
w16(len(b)); buf.extend(b)
w16(len(entry["components"]))
wc(entry["realm"])
for c in entry["components"]:
wc(c)
w32(entry["name_type"])
w32(entry["timestamp"])
w8(entry["kvno"] & 0xFF)
w16(entry["etype"])
wc(entry["key"])
w32(entry["kvno"]) # 32-bit KVNO extension
return struct.pack(">I", len(buf)) + bytes(buf)
# --- Parse input keytab ---
with open(INPUT_PATH, "rb") as fh:
raw = fh.read()
magic = struct.unpack_from(">H", raw, 0)[0]
if magic != 0x0502:
sys.exit(f"Unexpected keytab magic: {magic:#06x} (expected 0x0502)")
pos = 2
entries = []
while pos < len(raw):
if pos + 4 > len(raw):
break
entry, pos = read_entry(raw, pos)
if entry is None:
break
entries.append(entry)
# --- Select target entries ---
def is_target(e):
return (
e["realm"] == TARGET_REALM
and e["components"] == TARGET_PRINCIPAL_COMPONENTS
and e["etype"] in (ETYPE_AES128, ETYPE_AES256)
)
target_entries = [e for e in entries if is_target(e)]
if not target_entries:
sys.exit(
f"No AES entries found for "
f"{'/'.join(c.decode() for c in TARGET_PRINCIPAL_COMPONENTS)}"
f"@{TARGET_REALM.decode()} in {INPUT_PATH}"
)
max_kvno = max(e["kvno"] for e in target_entries)
selected = [e for e in target_entries if e["kvno"] == max_kvno]
print(f"Selected {len(selected)} entries for KVNO={max_kvno}, "
f"etypes={[e['etype'] for e in selected]}")
# --- Rewrite principal name ---
out_entries = []
for e in selected:
renamed = dict(e)
renamed["components"] = OUTPUT_COMPONENTS
renamed["name_type"] = OUTPUT_NAME_TYPE
renamed["realm"] = OUTPUT_REALM
out_entries.append(renamed)
# --- Write output keytab ---
with open(OUTPUT_PATH, "wb") as fh:
fh.write(struct.pack(">H", 0x0502))
for e in out_entries:
fh.write(pack_entry(e))
print(f"Wrote {len(out_entries)} entries to {OUTPUT_PATH}")
register: _gitea_rekey
changed_when: _gitea_rekey.rc == 0
failed_when: _gitea_rekey.rc != 0
tags: [spnego, keytab, gitea]
- name: Print keytab rekey output
ansible.builtin.debug:
msg: "{{ _gitea_rekey.stdout_lines }}"
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 9. Verify the output keytab contains the expected SPN principal
# ------------------------------------------------------------------
- name: Verify /tmp/git-http.keytab contains HTTP/git.prole.org@PROLE.ORG
ansible.builtin.command:
argv:
- klist
- -k
- -e
- /tmp/git-http.keytab
register: _gitea_klist
changed_when: false
failed_when: >-
_gitea_klist.rc != 0
or 'HTTP/git.prole.org@PROLE.ORG' not in _gitea_klist.stdout
tags: [spnego, keytab, gitea]
- name: Print klist output for verification
ansible.builtin.debug:
msg: "{{ _gitea_klist.stdout_lines }}"
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 10. Fetch keytab to control node
# ------------------------------------------------------------------
- name: Fetch git-http.keytab from remote to control node
ansible.builtin.fetch:
src: /tmp/git-http.keytab
dest: "{{ gitea_spnego_keytab_local }}"
flat: true
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 11. Cleanup temp files on the remote host
# ------------------------------------------------------------------
- name: Remove /tmp/full-domain.keytab from remote
ansible.builtin.file:
path: /tmp/full-domain.keytab
state: absent
tags: [spnego, keytab, gitea]
- name: Remove /tmp/git-http.keytab from remote
ansible.builtin.file:
path: /tmp/git-http.keytab
state: absent
tags: [spnego, keytab, gitea]
# ===========================================================================
# Play 2 — Kubernetes: apply secret and restart gitea-spnego-proxy
# ===========================================================================
- name: Apply SPNEGO keytab secret and restart gitea-spnego-proxy in Kubernetes
hosts: localhost
gather_facts: false
become: false
vars:
gitea_kube_context: prole-service-cluster
gitea_spnego_namespace: gitea
gitea_spnego_keytab_local: /tmp/git-http-fetched.keytab
tasks:
# ------------------------------------------------------------------
# 12. Apply keytab as Kubernetes secret gitea-krb5-keytab
#
# Uses --dry-run=client | apply to be fully idempotent — creates
# on first run, patches on subsequent runs.
# ------------------------------------------------------------------
- name: Apply gitea-krb5-keytab secret in namespace {{ gitea_spnego_namespace }}
ansible.builtin.shell:
cmd: |
set -o pipefail
kubectl --context={{ gitea_kube_context }} \
-n {{ gitea_spnego_namespace }} \
create secret generic gitea-krb5-keytab \
--from-file=http.keytab={{ gitea_spnego_keytab_local }} \
--dry-run=client -o yaml \
| kubectl --context={{ gitea_kube_context }} apply -f -
executable: /bin/bash
register: _gitea_secret
changed_when: "'configured' in _gitea_secret.stdout or 'created' in _gitea_secret.stdout"
failed_when: _gitea_secret.rc != 0
tags: [spnego, keytab, gitea]
- name: Print kubectl apply output for secret
ansible.builtin.debug:
msg: "{{ _gitea_secret.stdout_lines }}"
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 13. Restart gitea-spnego-proxy deployment to pick up new keytab
# ------------------------------------------------------------------
- name: Restart deployment/gitea-spnego-proxy
ansible.builtin.command:
argv:
- kubectl
- "--context={{ gitea_kube_context }}"
- -n
- "{{ gitea_spnego_namespace }}"
- rollout
- restart
- deployment/gitea-spnego-proxy
register: _gitea_restart
changed_when: _gitea_restart.rc == 0
failed_when: _gitea_restart.rc != 0
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 14. Wait for rollout to complete (timeout: 120s)
# ------------------------------------------------------------------
- name: Wait for gitea-spnego-proxy rollout to complete
ansible.builtin.command:
argv:
- kubectl
- "--context={{ gitea_kube_context }}"
- -n
- "{{ gitea_spnego_namespace }}"
- rollout
- status
- deployment/gitea-spnego-proxy
- "--timeout=120s"
register: _gitea_rollout
changed_when: false
failed_when: _gitea_rollout.rc != 0
tags: [spnego, keytab, gitea]
- name: Print rollout status
ansible.builtin.debug:
msg: "{{ _gitea_rollout.stdout_lines }}"
tags: [spnego, keytab, gitea]
# ------------------------------------------------------------------
# 15. Smoke test — confirm SPNEGO challenge (HTTP 401 + Negotiate)
#
# A correctly deployed SPNEGO proxy returns 401 with
# WWW-Authenticate: Negotiate on unauthenticated requests.
# We check the status code; a 200 would mean auth is disabled,
# anything other than 401 means the proxy is misconfigured.
# ------------------------------------------------------------------
- name: Smoke test — https://git.prole.org/ should return HTTP 401 (SPNEGO challenge)
ansible.builtin.command:
argv:
- curl
- -s
- -o
- /dev/null
- -w
- "%{http_code}"
- https://git.prole.org/
register: _gitea_smoke
changed_when: false
failed_when: _gitea_smoke.stdout | trim != "401"
tags: [spnego, keytab, gitea]
- name: Print smoke test result
ansible.builtin.debug:
msg: >-
https://git.prole.org/ returned HTTP {{ _gitea_smoke.stdout | trim }}.
SPNEGO challenge confirmed — proxy is issuing Negotiate header.
tags: [spnego, keytab, gitea]