mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 20:04:31 +00:00
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>
98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
goidentity "github.com/jcmturner/goidentity/v6"
|
|
"github.com/jcmturner/gokrb5/v8/keytab"
|
|
"github.com/jcmturner/gokrb5/v8/spnego"
|
|
)
|
|
|
|
const (
|
|
upstreamURL = "http://gitea-http.gitea.svc.cluster.local:3000"
|
|
listenAddr = ":4000"
|
|
)
|
|
|
|
// statusCapture wraps http.ResponseWriter to capture the HTTP status code
|
|
// written by a downstream handler so callers can inspect it after ServeHTTP.
|
|
type statusCapture struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (s *statusCapture) WriteHeader(code int) {
|
|
s.status = code
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func main() {
|
|
keytabPath := os.Getenv("KRB5_KTNAME")
|
|
if keytabPath == "" {
|
|
keytabPath = "/etc/krb5/http.keytab"
|
|
}
|
|
|
|
kt, err := keytab.Load(keytabPath)
|
|
if err != nil {
|
|
log.Fatalf("failed to load keytab %s: %v", keytabPath, err)
|
|
}
|
|
|
|
upstream, _ := url.Parse(upstreamURL)
|
|
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
|
proxy.Director = func(req *http.Request) {
|
|
req.URL.Scheme = upstream.Scheme
|
|
req.URL.Host = upstream.Host
|
|
if _, ok := req.Header["User-Agent"]; !ok {
|
|
req.Header.Set("User-Agent", "")
|
|
}
|
|
}
|
|
|
|
// authedInner runs only when SPNEGO negotiation succeeds.
|
|
// It extracts the client username from the request context and injects
|
|
// X-WEBAUTH-USER for Gitea reverse-proxy auto-login.
|
|
authedInner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// goidentity.AddToHTTPRequestContext is called by SPNEGOKRB5Authenticate on success.
|
|
// FromHTTPRequestContext retrieves the identity set there.
|
|
if id := goidentity.FromHTTPRequestContext(r); id != nil {
|
|
username := strings.SplitN(id.UserName(), "@", 2)[0]
|
|
r.Header.Set("X-WEBAUTH-USER", username)
|
|
log.Printf("SPNEGO auth OK: %s -> %s", id.UserName(), username)
|
|
}
|
|
proxy.ServeHTTP(w, r)
|
|
})
|
|
|
|
spnegoHandler := spnego.SPNEGOKRB5Authenticate(authedInner, kt)
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/_healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("ok"))
|
|
})
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.Header.Get("Authorization"), "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).
|
|
rw := &statusCapture{ResponseWriter: w}
|
|
spnegoHandler.ServeHTTP(rw, r)
|
|
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 "):])
|
|
}
|
|
return
|
|
}
|
|
// No Kerberos ticket — pass through so Gitea can show its own login page.
|
|
proxy.ServeHTTP(w, r)
|
|
})
|
|
|
|
log.Printf("spnego-proxy listening on %s, upstream %s", listenAddr, upstreamURL)
|
|
log.Fatal(http.ListenAndServe(listenAddr, mux))
|
|
}
|