Move backends to cmd args, allow setting private key seed via parameter or ENV var

This commit is contained in:
WeebDataHoarder
2025-04-06 03:08:19 +02:00
parent c763a59a4d
commit 411f028f56
7 changed files with 176 additions and 72 deletions

View File

@@ -28,7 +28,7 @@ type ChallengeInformation struct {
IssuedAt *jwt.NumericDate `json:"iat,omitempty"`
}
func (state *State) GetRequestAddress(r *http.Request) net.IP {
func getRequestAddress(r *http.Request) net.IP {
//TODO: verified upstream
ipStr := r.Header.Get("X-Real-Ip")
if ipStr == "" {
@@ -47,7 +47,7 @@ func (state *State) GetChallengeKeyForRequest(name string, until time.Time, r *h
hasher.Write([]byte("challenge\x00"))
hasher.Write([]byte(name))
hasher.Write([]byte{0})
hasher.Write(state.GetRequestAddress(r).To16())
hasher.Write(getRequestAddress(r).To16())
hasher.Write([]byte{0})
// specific headers
@@ -64,7 +64,7 @@ func (state *State) GetChallengeKeyForRequest(name string, until time.Time, r *h
hasher.Write([]byte{0})
_ = binary.Write(hasher, binary.LittleEndian, until.UTC().Unix())
hasher.Write([]byte{0})
hasher.Write(state.PublicKey)
hasher.Write(state.publicKey)
hasher.Write([]byte{0})
return hasher.Sum(nil)
@@ -73,7 +73,7 @@ func (state *State) GetChallengeKeyForRequest(name string, until time.Time, r *h
func (state *State) IssueChallengeToken(name string, key, result []byte, until time.Time) (token string, err error) {
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.EdDSA,
Key: state.PrivateKey,
Key: state.privateKey,
}, nil)
if err != nil {
return "", err
@@ -135,7 +135,7 @@ func (state *State) VerifyChallengeToken(name string, expectedKey []byte, w http
}
var i ChallengeInformation
err = token.Claims(state.PublicKey, &i)
err = token.Claims(state.publicKey, &i)
if err != nil {
return false, err
}

View File

@@ -3,7 +3,6 @@ package lib
import (
"bytes"
"codeberg.org/meta/gzipped/v2"
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
@@ -11,16 +10,12 @@ import (
"fmt"
"git.gammaspectra.live/git/go-away/embed"
"git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/utils"
"github.com/google/cel-go/common/types"
"html/template"
"io"
"log/slog"
"maps"
"net"
"net/http"
"net/http/httputil"
"net/url"
"path"
"path/filepath"
"strconv"
@@ -72,34 +67,6 @@ func initTemplate(name, data string) error {
return nil
}
func makeReverseProxy(target string) (*httputil.ReverseProxy, error) {
u, err := url.Parse(target)
if err != nil {
return nil, fmt.Errorf("failed to parse target URL: %w", err)
}
transport := http.DefaultTransport.(*http.Transport).Clone()
// https://github.com/oauth2-proxy/oauth2-proxy/blob/4e2100a2879ef06aea1411790327019c1a09217c/pkg/upstream/http.go#L124
if u.Scheme == "unix" {
// clean path up so we don't use the socket path in proxied requests
addr := u.Path
u.Path = ""
// tell transport how to dial unix sockets
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, "unix", addr)
}
// tell transport how to handle the unix url scheme
transport.RegisterProtocol("unix", utils.UnixRoundTripper{Transport: transport})
}
rp := httputil.NewSingleHostReverseProxy(u)
rp.Transport = transport
return rp, nil
}
func (state *State) challengePage(w http.ResponseWriter, id string, status int, challenge string, params map[string]any) error {
input := make(map[string]any)
input["Id"] = id
@@ -158,10 +125,10 @@ func (state *State) addTiming(w http.ResponseWriter, name, desc string, duration
}
}
func (state *State) getLogger(r *http.Request) *slog.Logger {
func GetLoggerForRequest(r *http.Request) *slog.Logger {
return slog.With(
"request_id", r.Header.Get("X-Away-Id"),
"remote_address", state.GetRequestAddress(r),
"remote_address", getRequestAddress(r),
"user_agent", r.UserAgent(),
"host", r.Host,
"path", r.URL.Path,
@@ -172,13 +139,13 @@ func (state *State) getLogger(r *http.Request) *slog.Logger {
func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
host := r.Host
backend, ok := state.Backends[host]
backend, ok := state.Settings.Backends[host]
if !ok {
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
return
}
lg := state.getLogger(r)
lg := GetLoggerForRequest(r)
start := time.Now()
@@ -186,7 +153,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
env := map[string]any{
"host": host,
"method": r.Method,
"remoteAddress": state.GetRequestAddress(r),
"remoteAddress": getRequestAddress(r),
"userAgent": r.UserAgent(),
"path": r.URL.Path,
"query": func() map[string]string {
@@ -292,7 +259,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
if rule.Action == policy.RuleActionCHECK {
goto nextRule
}
state.getLogger(r).Warn("challenge passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
GetLoggerForRequest(r).Warn("challenge passed", "rule", rule.Name, "rule_hash", rule.Hash, "challenge", challengeName)
// we pass the challenge early!
r.Header.Set(fmt.Sprintf("X-Away-Challenge-%s-Verify", challengeName), "PASS")
@@ -407,15 +374,15 @@ func (state *State) setupRoutes() error {
state.addTiming(w, "challenge-verify", "Verify client challenge", time.Since(start))
if err != nil {
state.getLogger(r).Error(fmt.Errorf("challenge error: %w", err).Error(), "challenge", challengeName, "redirect", r.FormValue("redirect"))
GetLoggerForRequest(r).Error(fmt.Errorf("challenge error: %w", err).Error(), "challenge", challengeName, "redirect", r.FormValue("redirect"))
return err
} else if !ok {
state.getLogger(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
GetLoggerForRequest(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
ClearCookie(CookiePrefix+challengeName, w)
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", challengeName))
return nil
}
state.getLogger(r).Info("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
GetLoggerForRequest(r).Info("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
token, err := state.IssueChallengeToken(challengeName, key, []byte(result), expiry)
if err != nil {

View File

@@ -44,5 +44,7 @@ type Policy struct {
Rules []Rule `yaml:"rules"`
// Backends
// Deprecated
Backends map[string]string `json:"backends"`
}

View File

@@ -29,6 +29,7 @@ import (
"log/slog"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
@@ -42,7 +43,6 @@ type State struct {
Settings StateSettings
UrlPath string
Mux *http.ServeMux
Backends map[string]http.Handler
Networks map[string]cidranger.Ranger
@@ -55,8 +55,8 @@ type State struct {
Rules []RuleState
PublicKey ed25519.PublicKey
PrivateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
Poison map[string][]byte
}
@@ -100,6 +100,8 @@ type ChallengeState struct {
}
type StateSettings struct {
Backends map[string]http.Handler
PrivateKeySeed []byte
Debug bool
PackageName string
ChallengeTemplate string
@@ -116,25 +118,36 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
}
state.UrlPath = "/.well-known/." + state.Settings.PackageName
state.Backends = make(map[string]http.Handler)
// set a reasonable configuration for default http proxy if there is none
for _, backend := range state.Settings.Backends {
if proxy, ok := backend.(*httputil.ReverseProxy); ok {
if proxy.ErrorHandler == nil {
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
GetLoggerForRequest(r).Error(err.Error())
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusBadGateway, err)
}
}
}
}
for k, v := range p.Backends {
backend, err := makeReverseProxy(v)
if len(state.Settings.PrivateKeySeed) > 0 {
if len(state.Settings.PrivateKeySeed) != ed25519.SeedSize {
return nil, fmt.Errorf("invalid private key seed length: %d", len(state.Settings.PrivateKeySeed))
}
state.privateKey = ed25519.NewKeyFromSeed(state.Settings.PrivateKeySeed)
state.publicKey = state.privateKey.Public().(ed25519.PublicKey)
clear(state.Settings.PrivateKeySeed)
} else {
state.publicKey, state.privateKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("backend %s: failed to make reverse proxy: %w", k, err)
return nil, err
}
backend.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
state.getLogger(r).Error(fmt.Errorf("backend %s error: %w", k, err).Error())
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusBadGateway, err)
}
state.Backends[k] = backend
}
state.PublicKey, state.PrivateKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
privateKeyFingerprint := sha256.Sum256(state.PrivateKey)
privateKeyFingerprint := sha256.Sum256(state.privateKey)
if state.Settings.ChallengeTemplate == "" {
state.Settings.ChallengeTemplate = "anubis"
@@ -423,12 +436,12 @@ func NewState(p policy.Policy, settings StateSettings) (state *State, err error)
if ok, err := c.Verify(key, result); err != nil {
return err
} else if !ok {
state.getLogger(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
GetLoggerForRequest(r).Warn("challenge failed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
ClearCookie(CookiePrefix+challengeName, w)
_ = state.errorPage(w, r.Header.Get("X-Away-Id"), http.StatusForbidden, fmt.Errorf("access denied: failed challenge %s", challengeName))
return nil
}
state.getLogger(r).Warn("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
GetLoggerForRequest(r).Warn("challenge passed", "challenge", challengeName, "redirect", r.FormValue("redirect"))
token, err := state.IssueChallengeToken(challengeName, key, []byte(result), expiry)
if err != nil {