start of the secrets cache (based on sturdyc) (#987)

This commit is contained in:
Michael Quigley 2025-06-18 16:18:37 -04:00
parent a993ddabda
commit 4c5f3e77e3
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
5 changed files with 95 additions and 17 deletions

View File

@ -3,6 +3,7 @@ package publicProxy
import ( import (
"context" "context"
"crypto/md5" "crypto/md5"
"time"
"github.com/michaelquigley/cf" "github.com/michaelquigley/cf"
"github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints"
@ -22,6 +23,7 @@ type Config struct {
Interstitial *InterstitialConfig Interstitial *InterstitialConfig
Oauth *OauthConfig Oauth *OauthConfig
SecretsAccess *SecretsAccessConfig SecretsAccess *SecretsAccessConfig
SecretsCache *SecretsCacheConfig
Tls *endpoints.TlsConfig Tls *endpoints.TlsConfig
} }
@ -57,6 +59,13 @@ type SecretsAccessConfig struct {
ServiceName string ServiceName string
} }
type SecretsCacheConfig struct {
Capacity int
Shards int
TTL time.Duration
EvictionPercentage int
}
func (p *OauthProviderConfig) GetEndpoint() oauth2.Endpoint { func (p *OauthProviderConfig) GetEndpoint() oauth2.Endpoint {
return oauth2.Endpoint{ return oauth2.Endpoint{
AuthURL: p.AuthURL, AuthURL: p.AuthURL,
@ -68,6 +77,12 @@ func DefaultConfig() *Config {
return &Config{ return &Config{
Identity: "public", Identity: "public",
Address: "0.0.0.0:8080", Address: "0.0.0.0:8080",
SecretsCache: &SecretsCacheConfig{
Capacity: 10000,
Shards: 10,
TTL: 2 * time.Hour,
EvictionPercentage: 10,
},
} }
} }

View File

@ -153,18 +153,18 @@ func hostTargetReverseProxy(cfg *Config, ctx ziti.Context) *httputil.ReverseProx
return &httputil.ReverseProxy{Director: director} return &httputil.ReverseProxy{Director: director}
} }
func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Context) http.HandlerFunc { func shareHandler(handler http.Handler, cfg *Config, key []byte, ctx ziti.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
shrToken := resolveService(pcfg.HostMatch, r.Host) shrToken := resolveService(cfg.HostMatch, r.Host)
if shrToken != "" { if shrToken != "" {
if svc, found := endpoints.GetRefreshedService(shrToken, ctx); found { if svc, found := endpoints.GetRefreshedService(shrToken, ctx); found {
if cfg, found := svc.Config[sdk.ZrokProxyConfig]; found { if proxyConfig, found := svc.Config[sdk.ZrokProxyConfig]; found {
if r.Method != http.MethodOptions && (pcfg.Interstitial != nil && pcfg.Interstitial.Enabled) { if r.Method != http.MethodOptions && (cfg.Interstitial != nil && cfg.Interstitial.Enabled) {
sendInterstitial := true sendInterstitial := true
if len(pcfg.Interstitial.UserAgentPrefixes) > 0 { if len(cfg.Interstitial.UserAgentPrefixes) > 0 {
ua := r.Header.Get("User-Agent") ua := r.Header.Get("User-Agent")
matched := false matched := false
for _, prefix := range pcfg.Interstitial.UserAgentPrefixes { for _, prefix := range cfg.Interstitial.UserAgentPrefixes {
if strings.HasPrefix(ua, prefix) { if strings.HasPrefix(ua, prefix) {
matched = true matched = true
break break
@ -175,13 +175,13 @@ func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Conte
} }
} }
if sendInterstitial { if sendInterstitial {
if v, istlFound := cfg["interstitial"]; istlFound { if v, istlFound := proxyConfig["interstitial"]; istlFound {
if istlEnabled, ok := v.(bool); ok && istlEnabled { if istlEnabled, ok := v.(bool); ok && istlEnabled {
skip := r.Header.Get("skip_zrok_interstitial") skip := r.Header.Get("skip_zrok_interstitial")
_, zrokOkErr := r.Cookie("zrok_interstitial") _, zrokOkErr := r.Cookie("zrok_interstitial")
if skip == "" && zrokOkErr != nil { if skip == "" && zrokOkErr != nil {
logrus.Debugf("forcing interstitial for '%v'", r.URL) logrus.Debugf("forcing interstitial for '%v'", r.URL)
interstitialUi.WriteInterstitialAnnounce(w, pcfg.Interstitial.HtmlPath) interstitialUi.WriteInterstitialAnnounce(w, cfg.Interstitial.HtmlPath)
return return
} }
} }
@ -189,7 +189,7 @@ func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Conte
} }
} }
if scheme, found := cfg["auth_scheme"]; found { if scheme, found := proxyConfig["auth_scheme"]; found {
switch scheme { switch scheme {
case string(sdk.None): case string(sdk.None):
logrus.Debugf("auth scheme none '%v'", shrToken) logrus.Debugf("auth scheme none '%v'", shrToken)
@ -206,7 +206,7 @@ func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Conte
return return
} }
authed := false authed := false
if v, found := cfg["basic_auth"]; found { if v, found := proxyConfig["basic_auth"]; found {
if basicAuth, ok := v.(map[string]interface{}); ok { if basicAuth, ok := v.(map[string]interface{}); ok {
if v, found := basicAuth["users"]; found { if v, found := basicAuth["users"]; found {
if arr, ok := v.([]interface{}); ok { if arr, ok := v.([]interface{}); ok {
@ -247,7 +247,7 @@ func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Conte
case string(sdk.Oauth): case string(sdk.Oauth):
logrus.Debugf("auth scheme oauth '%v'", shrToken) logrus.Debugf("auth scheme oauth '%v'", shrToken)
if oauthCfg, found := cfg["oauth"]; found { if oauthCfg, found := proxyConfig["oauth"]; found {
if provider, found := oauthCfg.(map[string]interface{})["provider"]; found { if provider, found := oauthCfg.(map[string]interface{})["provider"]; found {
var authCheckInterval time.Duration var authCheckInterval time.Duration
if checkInterval, found := oauthCfg.(map[string]interface{})["authorization_check_interval"]; !found { if checkInterval, found := oauthCfg.(map[string]interface{})["authorization_check_interval"]; !found {
@ -268,34 +268,34 @@ func shareHandler(handler http.Handler, pcfg *Config, key []byte, ctx ziti.Conte
cookie, err := r.Cookie("zrok-access") cookie, err := r.Cookie("zrok-access")
if err != nil { if err != nil {
logrus.Errorf("unable to get 'zrok-access' cookie: %v", err) logrus.Errorf("unable to get 'zrok-access' cookie: %v", err)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval) oauthLoginRequired(w, r, cfg.Oauth, provider.(string), target, authCheckInterval)
return return
} }
tkn, err := jwt.ParseWithClaims(cookie.Value, &ZrokClaims{}, func(t *jwt.Token) (interface{}, error) { tkn, err := jwt.ParseWithClaims(cookie.Value, &ZrokClaims{}, func(t *jwt.Token) (interface{}, error) {
if pcfg.Oauth == nil { if cfg.Oauth == nil {
return nil, fmt.Errorf("missing oauth configuration for access point; unable to parse jwt") return nil, fmt.Errorf("missing oauth configuration for access point; unable to parse jwt")
} }
return key, nil return key, nil
}) })
if err != nil { if err != nil {
logrus.Errorf("unable to parse jwt: %v", err) logrus.Errorf("unable to parse jwt: %v", err)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval) oauthLoginRequired(w, r, cfg.Oauth, provider.(string), target, authCheckInterval)
return return
} }
claims := tkn.Claims.(*ZrokClaims) claims := tkn.Claims.(*ZrokClaims)
if claims.Provider != provider { if claims.Provider != provider {
logrus.Error("provider mismatch; restarting auth flow") logrus.Error("provider mismatch; restarting auth flow")
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval) oauthLoginRequired(w, r, cfg.Oauth, provider.(string), target, authCheckInterval)
return return
} }
if claims.AuthorizationCheckInterval != authCheckInterval { if claims.AuthorizationCheckInterval != authCheckInterval {
logrus.Error("authorization check interval mismatch; restarting auth flow") logrus.Error("authorization check interval mismatch; restarting auth flow")
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval) oauthLoginRequired(w, r, cfg.Oauth, provider.(string), target, authCheckInterval)
return return
} }
if claims.Audience != r.Host { if claims.Audience != r.Host {
logrus.Errorf("audience claim '%s' does not match requested host '%s'; restarting auth flow", claims.Audience, r.Host) logrus.Errorf("audience claim '%s' does not match requested host '%s'; restarting auth flow", claims.Audience, r.Host)
oauthLoginRequired(w, r, pcfg.Oauth, provider.(string), target, authCheckInterval) oauthLoginRequired(w, r, cfg.Oauth, provider.(string), target, authCheckInterval)
return return
} }

View File

@ -0,0 +1,60 @@
package publicProxy
import (
"context"
"net"
"time"
"github.com/openziti/sdk-golang/ziti"
"github.com/openziti/zrok/controller/secretsGrpc"
"github.com/viccon/sturdyc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
)
type Secret struct {
Key string
Value string
}
func GetSecrets(shareToken string, cfg *Config) ([]Secret, error) {
cacheClient := sturdyc.New[[]Secret](cfg.SecretsCache.Capacity, cfg.SecretsCache.Shards, cfg.SecretsCache.TTL, cfg.SecretsCache.EvictionPercentage)
fetch := func(ctx context.Context) ([]Secret, error) {
opts := []grpc.DialOption{
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
zcfg, err := ziti.NewConfigFromFile(cfg.SecretsAccess.IdentityPath)
if err != nil {
return nil, err
}
zctx, err := ziti.NewContext(zcfg)
if err != nil {
return nil, err
}
conn, err := zctx.DialWithOptions(addr, &ziti.DialOptions{ConnectTimeout: 30 * time.Second})
if err != nil {
return nil, err
}
return conn, nil
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
resolver.SetDefaultScheme("passthrough")
conn, err := grpc.NewClient(cfg.SecretsAccess.ServiceName, opts...)
if err != nil {
return nil, err
}
defer conn.Close()
client := secretsGrpc.NewSecretsClient(conn)
resp, err := client.FetchSecrets(ctx, &secretsGrpc.SecretsRequest{ShareToken: shareToken})
if err != nil {
return nil, err
}
var secrets []Secret
for _, secret := range resp.GetSecrets() {
secrets = append(secrets, Secret{Key: secret.Key, Value: secret.Value})
}
return secrets, nil
}
return cacheClient.GetOrFetch(context.Background(), shareToken, fetch)
}

1
go.mod
View File

@ -55,6 +55,7 @@ require (
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/viccon/sturdyc v1.1.5
github.com/wneessen/go-mail v0.2.7 github.com/wneessen/go-mail v0.2.7
github.com/zitadel/oidc/v3 v3.39.0 github.com/zitadel/oidc/v3 v3.39.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0

2
go.sum
View File

@ -940,6 +940,8 @@ github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOH
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/viccon/sturdyc v1.1.5 h1:GLQDnsyKt3L/tpdWCIARIRefn+5DAyvqu+0irBwt+vk=
github.com/viccon/sturdyc v1.1.5/go.mod h1:OCBEgG/i48uugKQ498UQlfMHmf5j8MYY8a4BApfVnMo=
github.com/wneessen/go-mail v0.2.7 h1:4gj1flZjm05htmVj8AS6TbYXLQBYabzuQMmu8pZc/Js= github.com/wneessen/go-mail v0.2.7 h1:4gj1flZjm05htmVj8AS6TbYXLQBYabzuQMmu8pZc/Js=
github.com/wneessen/go-mail v0.2.7/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E= github.com/wneessen/go-mail v0.2.7/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=