From 0e628474493e7b8ba05396d75f4e726fc960d709 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Fri, 3 Oct 2025 15:11:45 -1000 Subject: [PATCH 01/11] Customize tokenizer for my own use as a somewhat open tokenizer proxy. Require fly src be present when REQUIRE_FLY_SRC=true. --- cmd/tokenizer/main.go | 4 ++++ tokenizer.go | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/cmd/tokenizer/main.go b/cmd/tokenizer/main.go index 352561f..af755c8 100644 --- a/cmd/tokenizer/main.go +++ b/cmd/tokenizer/main.go @@ -89,6 +89,10 @@ func runServe() { opts = append(opts, tokenizer.OpenProxy()) } + if slices.Contains([]string{"1", "true"}, os.Getenv("REQUIRE_FLY_SRC")) { + opts = append(opts, tokenizer.RequireFlySrc()) + } + tkz := tokenizer.NewTokenizer(key, opts...) if len(os.Getenv("DEBUG")) != 0 { diff --git a/tokenizer.go b/tokenizer.go index 1cfa27f..33328e3 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -23,6 +23,8 @@ import ( "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" "golang.org/x/exp/maps" + + "github.com/superfly/tokenizer/flysrc" ) var FilteredHeaders = []string{headerProxyAuthorization, headerProxyTokenizer} @@ -46,6 +48,9 @@ type tokenizer struct { // OpenProxy dictates whether requests without any sealed secrets are allowed. OpenProxy bool + // RequireFlySrc will reject requests without a fly-src when set. + RequireFlySrc bool + // tokenizerHostnames is a list of hostnames where tokenizer can be reached. // If provided, this allows tokenizer to transparently proxy requests (ie. // accept normal HTTP requests with arbitrary hostnames) while blocking @@ -66,6 +71,13 @@ func OpenProxy() Option { } } +// RequireFlySrc specifies that requests without a fly-src will be rejected. +func RequireFlySrc() Option { + return func(t *tokenizer) { + t.RequireFlySrc = true + } +} + // TokenizerHostnames is a list of hostnames where tokenizer can be reached. If // provided, this allows tokenizer to transparently proxy requests (ie. accept // normal HTTP requests with arbitrary hostnames) while blocking circular @@ -224,6 +236,21 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht "queryKeys": strings.Join(maps.Keys(req.URL.Query()), ", "), }) + flysrc, err := flysrc.FromRequest(req) + if err != nil { + if t.RequireFlySrc { + pud.reqLog.Warn(err.Error()) + return nil, errorResponse(ErrBadRequest) + } + } else { + pud.reqLog = pud.reqLog.WithFields(logrus.Fields{ + "flysrc-org": flysrc.Org, + "flysrc-app": flysrc.App, + "flysrc-instance": flysrc.Instance, + "flysrc-timestamp": flysrc.Timestamp, + }) + } + processors := append([]RequestProcessor(nil), pud.connectProcessors...) if reqProcessors, err := t.processorsFromRequest(req); err != nil { pud.reqLog.WithError(err).Warn("find processor") From 09d23b4bb960e8795b9258b2599c0f1bdd771bc1 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Tue, 7 Oct 2025 13:46:32 -1000 Subject: [PATCH 02/11] increase logging and include a tool for specific type of token sealing. --- authorizer.go | 27 +++++++++++++ cmd/sealtoken/main.go | 90 +++++++++++++++++++++++++++++++++++++++++++ flysrc/context.go | 20 ++++++++++ flysrc/fly_src.go | 4 ++ secret.go | 13 +++++++ tokenizer.go | 85 +++++++++++++++++++++++++++------------- 6 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 cmd/sealtoken/main.go create mode 100644 flysrc/context.go diff --git a/authorizer.go b/authorizer.go index abb98fe..68d3a22 100644 --- a/authorizer.go +++ b/authorizer.go @@ -28,8 +28,15 @@ const ( maxFlySrcAge = 30 * time.Second ) +var redactedBase64 []byte + +func init() { + redactedBase64, _ = base64.StdEncoding.DecodeString("REDACTED") +} + type AuthConfig interface { AuthRequest(req *http.Request) error + StripHazmat() AuthConfig } type wireAuth struct { @@ -110,6 +117,10 @@ func (c *BearerAuthConfig) AuthRequest(req *http.Request) error { return fmt.Errorf("%w: bad or missing proxy auth", ErrNotAuthorized) } +func (c *BearerAuthConfig) StripHazmat() AuthConfig { + return &BearerAuthConfig{redactedBase64} +} + type MacaroonAuthConfig struct { Key []byte `json:"key"` } @@ -150,6 +161,10 @@ func (c *MacaroonAuthConfig) AuthRequest(req *http.Request) error { return fmt.Errorf("%w: bad or missing proxy auth", ErrNotAuthorized) } +func (c *MacaroonAuthConfig) StripHazmat() AuthConfig { + return &MacaroonAuthConfig{redactedBase64} +} + func (c *MacaroonAuthConfig) Macaroon(caveats ...macaroon.Caveat) (string, error) { m, err := macaroon.New(tkmac.KeyFingerprint(c.Key), tkmac.Location, c.Key) if err != nil { @@ -204,6 +219,10 @@ func (c *FlyioMacaroonAuthConfig) AuthRequest(req *http.Request) error { return fmt.Errorf("%w: bad or missing proxy auth", ErrNotAuthorized) } +func (c *FlyioMacaroonAuthConfig) StripHazmat() AuthConfig { + return c +} + // FlySrcAuthConfig allows permitting access to a secret based on the Fly-Src // header added to Flycast requests between Fly.io machines/apps/orgs. // https://community.fly.io/t/fly-src-authenticating-http-requests-between-fly-apps/20566 @@ -280,6 +299,10 @@ func (c *FlySrcAuthConfig) AuthRequest(req *http.Request) error { return nil } +func (c *FlySrcAuthConfig) StripHazmat() AuthConfig { + return c +} + type NoAuthConfig struct{} var _ AuthConfig = (*NoAuthConfig)(nil) @@ -288,6 +311,10 @@ func (c *NoAuthConfig) AuthRequest(req *http.Request) error { return nil } +func (c *NoAuthConfig) StripHazmat() AuthConfig { + return c +} + func proxyAuthorizationTokens(req *http.Request) (ret []string) { hdrLoop: for _, hdr := range req.Header.Values(headerProxyAuthorization) { diff --git a/cmd/sealtoken/main.go b/cmd/sealtoken/main.go new file mode 100644 index 0000000..3753f9f --- /dev/null +++ b/cmd/sealtoken/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/superfly/tokenizer" +) + +func wrapToken(token, sealKey, orgSlug, appSlug, targHost, targHdr string, debug bool) (string, error) { + inj := &tokenizer.InjectProcessorConfig{Token: token} + secret := tokenizer.Secret{ + AuthConfig: tokenizer.NewFlySrcAuthConfig( + tokenizer.AllowlistFlySrcOrgs(orgSlug), + tokenizer.AllowlistFlySrcApps(appSlug), + ), + ProcessorConfig: inj, + RequestValidators: []tokenizer.RequestValidator{ + tokenizer.AllowHosts(targHost), + }, + } + + // If they request a specific header, fill it in verbatim. + // Otherwise the tokenizer will default to filling in "Authorization: Bearer ". + if targHdr != "" { + inj.Fmt = "%s" + inj.Dst = targHdr + } + + if debug { + bs, err := json.Marshal(secret) + if err != nil { + return "", fmt.Errorf("json.Marshal: %w", err) + } + return string(bs), nil + } + + return secret.Seal(sealKey) +} + +func tryMain() error { + defSealKey := os.Getenv("SEAL_KEY") + sealKey := flag.String("sealkey", defSealKey, "tokenizer seal key, or from environment SEAL_KEY") + orgSlug := flag.String("org", "", "allowed org slug") + appSlug := flag.String("app", "", "allowed app slug") + targHost := flag.String("host", "", "target host") + targHdr := flag.String("header", "", "target header to fill. Defaults to the bearer authorization header") + debug := flag.Bool("debug", false, "show json of sealed secret") + + prog := os.Args[0] + flag.Parse() + args := flag.Args() + + if len(args) != 1 { + fmt.Printf("usage: %s [flags] token\n", prog) + flag.PrintDefaults() + return fmt.Errorf("token unspecified") + } + + if *sealKey == "" { + return fmt.Errorf("sealkey unspecified") + } + if *orgSlug == "" { + return fmt.Errorf("org unspecified") + } + if *appSlug == "" { + return fmt.Errorf("app unspecified") + } + if *targHost == "" { + return fmt.Errorf("target host unspecified") + } + + token := args[0] + + wrapped, err := wrapToken(token, *sealKey, *orgSlug, *appSlug, *targHost, *targHdr, *debug) + if err != nil { + return fmt.Errorf("wrapToken: %w", err) + } + fmt.Printf("%s\n", wrapped) + return nil +} + +func main() { + if err := tryMain(); err != nil { + fmt.Printf("%v\n", err) + os.Exit(1) + } +} diff --git a/flysrc/context.go b/flysrc/context.go new file mode 100644 index 0000000..16f2ce7 --- /dev/null +++ b/flysrc/context.go @@ -0,0 +1,20 @@ +package flysrc + +import ( + "context" +) + +type contextKey string + +const ( + contextKeyFlySrc = contextKey("fly-src") +) + +func WithFlySrc(ctx context.Context, fsrc *Parsed) context.Context { + return context.WithValue(ctx, contextKeyFlySrc, fsrc) +} + +func FlySrcFromContext(ctx context.Context) *Parsed { + fsrc, _ := ctx.Value(contextKeyFlySrc).(*Parsed) + return fsrc +} diff --git a/flysrc/fly_src.go b/flysrc/fly_src.go index fd79e17..d6a6ed0 100644 --- a/flysrc/fly_src.go +++ b/flysrc/fly_src.go @@ -33,6 +33,10 @@ type Parsed struct { } func FromRequest(req *http.Request) (*Parsed, error) { + if p := FlySrcFromContext(req.Context()); p != nil { + return p, nil + } + srcHdr := req.Header.Get(headerFlySrc) if srcHdr == "" { return nil, errors.New("missing Fly-Src header") diff --git a/secret.go b/secret.go index 1bbc095..dd58ced 100644 --- a/secret.go +++ b/secret.go @@ -49,6 +49,19 @@ func (s *Secret) sealRaw(key *[32]byte) (string, error) { return base64.StdEncoding.EncodeToString(sct), nil } +func (s *Secret) StripHazmat() *Secret { + return &Secret{ + AuthConfig: s.AuthConfig.StripHazmat(), + ProcessorConfig: s.ProcessorConfig, + RequestValidators: s.RequestValidators, + } +} + +func (s *Secret) StripHazmatString() string { + bs, _ := json.Marshal(s.StripHazmat()) + return string(bs) +} + type wireSecret struct { wireProcessor wireAuth diff --git a/tokenizer.go b/tokenizer.go index 33328e3..a0cb970 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -118,9 +118,11 @@ func NewTokenizer(openKey string, opts ...Option) *tokenizer { proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case len(hostnameMap) == 0: + logrus.WithFields(reqLogFields(r)).Warn("I'm not that kind of server") http.Error(w, "I'm not that kind of server", 400) return case r.Host == "": + logrus.WithFields(reqLogFields(r)).Warn("must specify host") http.Error(w, "must specify host", 400) return } @@ -130,12 +132,14 @@ func NewTokenizer(openKey string, opts ...Option) *tokenizer { if strings.Contains(err.Error(), "missing port in address") { host = r.Host } else { + logrus.WithFields(reqLogFields(r)).Warn("bad host") http.Error(w, "bad host", 400) return } } if hostnameMap[host] { + logrus.WithFields(reqLogFields(r)).Warn("circular request") http.Error(w, "circular request", 400) return } @@ -181,14 +185,16 @@ type proxyUserData struct { // HandleConnect implements goproxy.FuncHttpsHandler func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { + logger := logrus.WithField("connect_host", host) + logger = logger.WithFields(reqLogFields(ctx.Req)) if host == "" { - logrus.Warn("no host in CONNECT request") + logger.Warn("no host in CONNECT request") ctx.Resp = errorResponse(ErrBadRequest) return goproxy.RejectConnect, "" } pud := &proxyUserData{ - connLog: logrus.WithField("connect_host", host), + connLog: logger, connectStart: time.Now(), } @@ -199,18 +205,42 @@ func (t *tokenizer) HandleConnect(host string, ctx *goproxy.ProxyCtx) (*goproxy. return goproxy.RejectConnect, "" } - var err error - if pud.connectProcessors, err = t.processorsFromRequest(ctx.Req); err != nil { + connectProcessors, safeSecret, err := t.processorsFromRequest(ctx.Req) + if safeSecret != "" { + pud.connLog = pud.connLog.WithField("secret", safeSecret) + } + if err != nil { pud.connLog.WithError(err).Warn("find processor (CONNECT)") ctx.Resp = errorResponse(err) return goproxy.RejectConnect, "" } + pud.connectProcessors = connectProcessors ctx.UserData = pud return goproxy.HTTPMitmConnect, host } +func getSource(req *http.Request) string { + var forwards []string + if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" { + forwards = strings.Split(forwardedFor, ", ") + } + + srcs := append(forwards, req.RemoteAddr) + return strings.Join(srcs, ", ") +} + +func reqLogFields(req *http.Request) logrus.Fields { + return logrus.Fields{ + "source": getSource(req), + "method": req.Method, + "host": req.Host, + "path": req.URL.Path, + "queryKeys": strings.Join(maps.Keys(req.URL.Query()), ", "), + } +} + // HandleRequest implements goproxy.FuncReqHandler func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { if ctx.UserData == nil { @@ -227,32 +257,33 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht if pud.connLog != nil { pud.reqLog = pud.connLog } else { - pud.reqLog = logrus.StandardLogger() + pud.reqLog = logrus.WithFields(reqLogFields(ctx.Req)) } - pud.reqLog = pud.reqLog.WithFields(logrus.Fields{ - "method": req.Method, - "host": req.Host, - "path": req.URL.Path, - "queryKeys": strings.Join(maps.Keys(req.URL.Query()), ", "), - }) - flysrc, err := flysrc.FromRequest(req) + // XXX TODO: will fly src headers appear in each request within the proxy stream? + // XXX or do we have to get the fly src once for the connect message and share? + fsrc, err := flysrc.FromRequest(req) if err != nil { if t.RequireFlySrc { pud.reqLog.Warn(err.Error()) return nil, errorResponse(ErrBadRequest) } } else { + req = req.Clone(flysrc.WithFlySrc(req.Context(), fsrc)) pud.reqLog = pud.reqLog.WithFields(logrus.Fields{ - "flysrc-org": flysrc.Org, - "flysrc-app": flysrc.App, - "flysrc-instance": flysrc.Instance, - "flysrc-timestamp": flysrc.Timestamp, + "flysrc-org": fsrc.Org, + "flysrc-app": fsrc.App, + "flysrc-instance": fsrc.Instance, + "flysrc-timestamp": fsrc.Timestamp, }) } processors := append([]RequestProcessor(nil), pud.connectProcessors...) - if reqProcessors, err := t.processorsFromRequest(req); err != nil { + reqProcessors, safeSecret, err := t.processorsFromRequest(req) + if safeSecret != "" { + pud.reqLog = pud.reqLog.WithField("secret", safeSecret) + } + if err != nil { pud.reqLog.WithError(err).Warn("find processor") return req, errorResponse(err) } else { @@ -321,49 +352,51 @@ func (t *tokenizer) HandleResponse(resp *http.Response, ctx *goproxy.ProxyCtx) * return resp } -func (t *tokenizer) processorsFromRequest(req *http.Request) ([]RequestProcessor, error) { +func (t *tokenizer) processorsFromRequest(req *http.Request) ([]RequestProcessor, string, error) { hdrs := req.Header[headerProxyTokenizer] processors := make([]RequestProcessor, 0, len(hdrs)) + var safeSecret string for _, hdr := range hdrs { b64Secret, params, err := parseHeaderProxyTokenizer(hdr) if err != nil { - return nil, err + return nil, safeSecret, err } ctSecret, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64Secret)) if err != nil { - return nil, fmt.Errorf("bad Proxy-Tokenizer encoding: %w", err) + return nil, safeSecret, fmt.Errorf("bad Proxy-Tokenizer encoding: %w", err) } jsonSecret, ok := box.OpenAnonymous(nil, ctSecret, t.pub, t.priv) if !ok { - return nil, errors.New("failed Proxy-Tokenizer decryption") + return nil, safeSecret, errors.New("failed Proxy-Tokenizer decryption") } secret := new(Secret) if err = json.Unmarshal(jsonSecret, secret); err != nil { - return nil, fmt.Errorf("bad secret json: %w", err) + return nil, safeSecret, fmt.Errorf("bad secret json: %w", err) } + safeSecret = secret.StripHazmatString() if err = secret.AuthRequest(req); err != nil { - return nil, fmt.Errorf("unauthorized to use secret: %w", err) + return nil, safeSecret, fmt.Errorf("unauthorized to use secret: %w", err) } for _, v := range secret.RequestValidators { if err := v.Validate(req); err != nil { - return nil, fmt.Errorf("request validator failed: %w", err) + return nil, safeSecret, fmt.Errorf("request validator failed: %w", err) } } processor, err := secret.Processor(params) if err != nil { - return nil, err + return nil, safeSecret, err } processors = append(processors, processor) } - return processors, nil + return processors, safeSecret, nil } func parseHeaderProxyTokenizer(hdr string) (string, map[string]string, error) { From daa485a07d78f7218216af8778cb43ed648b9337 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Tue, 7 Oct 2025 14:08:30 -1000 Subject: [PATCH 03/11] strip hazmat from processors too --- authorizer.go | 1 + processor.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ secret.go | 2 +- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/authorizer.go b/authorizer.go index 68d3a22..84e897e 100644 --- a/authorizer.go +++ b/authorizer.go @@ -28,6 +28,7 @@ const ( maxFlySrcAge = 30 * time.Second ) +var redactedStr = "REDACTED" var redactedBase64 []byte func init() { diff --git a/processor.go b/processor.go index 7a31df1..e874f91 100644 --- a/processor.go +++ b/processor.go @@ -48,6 +48,7 @@ type RequestProcessor func(r *http.Request) error type ProcessorConfig interface { Processor(map[string]string) (RequestProcessor, error) + StripHazmat() ProcessorConfig } type wireProcessor struct { @@ -129,6 +130,15 @@ func (c *InjectProcessorConfig) Processor(params map[string]string) (RequestProc }, nil } +func (c *InjectProcessorConfig) StripHazmat() ProcessorConfig { + // DO NOT PUT HAZMAT INTO FmtProcessor or DstProcessor. + return &InjectProcessorConfig{ + Token: redactedStr, + FmtProcessor: c.FmtProcessor, + DstProcessor: c.DstProcessor, + } +} + type InjectHMACProcessorConfig struct { Key []byte `json:"key"` Hash string `json:"hash"` @@ -178,6 +188,16 @@ func (c *InjectHMACProcessorConfig) Processor(params map[string]string) (Request }, nil } +func (c *InjectHMACProcessorConfig) StripHazmat() ProcessorConfig { + // DO NOT PUT HAZMAT INTO FmtProcessor or DstProcessor. + return &InjectHMACProcessorConfig{ + Key: redactedBase64, + Hash: redactedStr, + FmtProcessor: c.FmtProcessor, + DstProcessor: c.DstProcessor, + } +} + type OAuthProcessorConfig struct { Token *OAuthToken `json:"token"` } @@ -205,6 +225,15 @@ func (c *OAuthProcessorConfig) Processor(params map[string]string) (RequestProce }, nil } +func (c *OAuthProcessorConfig) StripHazmat() ProcessorConfig { + return &OAuthProcessorConfig{ + Token: &OAuthToken{ + AccessToken: redactedStr, + RefreshToken: redactedStr, + }, + } +} + type Sigv4ProcessorConfig struct { AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` @@ -299,6 +328,13 @@ func (c *Sigv4ProcessorConfig) Processor(params map[string]string) (RequestProce }, nil } +func (c *Sigv4ProcessorConfig) StripHazmat() ProcessorConfig { + return &Sigv4ProcessorConfig{ + AccessKey: redactedStr, + SecretKey: redactedStr, + } +} + type MultiProcessorConfig []ProcessorConfig var _ ProcessorConfig = new(MultiProcessorConfig) @@ -323,6 +359,14 @@ func (c MultiProcessorConfig) Processor(params map[string]string) (RequestProces }, nil } +func (c *MultiProcessorConfig) StripHazmat() ProcessorConfig { + ret := make(MultiProcessorConfig, len(*c)) + for n, p := range *c { + ret[n] = p.StripHazmat() + } + return &ret +} + func (c MultiProcessorConfig) MarshalJSON() ([]byte, error) { wps := make([]wireProcessor, 0, len(c)) for _, p := range c { diff --git a/secret.go b/secret.go index dd58ced..d94bfe9 100644 --- a/secret.go +++ b/secret.go @@ -52,7 +52,7 @@ func (s *Secret) sealRaw(key *[32]byte) (string, error) { func (s *Secret) StripHazmat() *Secret { return &Secret{ AuthConfig: s.AuthConfig.StripHazmat(), - ProcessorConfig: s.ProcessorConfig, + ProcessorConfig: s.ProcessorConfig.StripHazmat(), RequestValidators: s.RequestValidators, } } From f93dbd22f88daeb4a0c24a97b6b498a9ecf045ec Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Tue, 7 Oct 2025 18:12:26 -1000 Subject: [PATCH 04/11] reject fly-src when it doesnt come from fly-proxy's netblock. --- flysrc/fly_src.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/flysrc/fly_src.go b/flysrc/fly_src.go index d6a6ed0..d8b49b7 100644 --- a/flysrc/fly_src.go +++ b/flysrc/fly_src.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "net" "net/http" "os" "strconv" @@ -25,6 +26,11 @@ const ( var VerifyKey = readFlySrcKey(flySrcSignatureKeyPath) +var flyProxyNet = net.IPNet{ + IP: net.ParseIP("172.16.0.0"), + Mask: net.CIDRMask(16, 32), +} + type Parsed struct { Org string App string @@ -47,6 +53,22 @@ func FromRequest(req *http.Request) (*Parsed, error) { return nil, errors.New("missing Fly-Src signature") } + host, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return nil, errors.New("fly-src from malformed remote address") + } + + peerIp := net.ParseIP(host) + if peerIp == nil { + return nil, errors.New("fly-src from malformed ip") + } + + // The fly-src is only trusted if it comes from fly-proxy, which + // means it will arrive to the service_ip (127.19.x.x/16) from 172.16.x.x/16. + if !flyProxyNet.Contains(peerIp) { + return nil, errors.New("fly-src is not from fly-proxy") + } + return verifyAndParseFlySrc(srcHdr, sigHdr, VerifyKey) } From 0c52427628bf9110e2ecdd0dba6f1939a9a45ecd Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Tue, 7 Oct 2025 21:52:19 -1000 Subject: [PATCH 05/11] allow fly-src from 127.0.0.1/16 in test cases. --- flysrc/fly_src.go | 4 ++-- tokenizer_test.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/flysrc/fly_src.go b/flysrc/fly_src.go index d8b49b7..2ead851 100644 --- a/flysrc/fly_src.go +++ b/flysrc/fly_src.go @@ -26,7 +26,7 @@ const ( var VerifyKey = readFlySrcKey(flySrcSignatureKeyPath) -var flyProxyNet = net.IPNet{ +var FlyProxyNet = net.IPNet{ IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(16, 32), } @@ -65,7 +65,7 @@ func FromRequest(req *http.Request) (*Parsed, error) { // The fly-src is only trusted if it comes from fly-proxy, which // means it will arrive to the service_ip (127.19.x.x/16) from 172.16.x.x/16. - if !flyProxyNet.Contains(peerIp) { + if !FlyProxyNet.Contains(peerIp) { return nil, errors.New("fly-src is not from fly-proxy") } diff --git a/tokenizer_test.go b/tokenizer_test.go index 3d35b78..2f268bc 100644 --- a/tokenizer_test.go +++ b/tokenizer_test.go @@ -29,6 +29,9 @@ import ( ) func TestTokenizer(t *testing.T) { + // honor fly-src when from 127.0.0.1/16 instead of from 172.16.0.0/16. + flysrc.FlyProxyNet.IP = net.ParseIP("127.0.0.1") + appServer := httptest.NewTLSServer(echo) defer appServer.Close() From 7e4fa7efbdb483afd71a896c56eb21035da8b32d Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Wed, 8 Oct 2025 13:12:52 -1000 Subject: [PATCH 06/11] add a quickstart doc from my notes, and apply a small fix. --- QuickStart.md | 65 +++++++++++++++++++++++++++++++++++++++++++ cmd/tokenizer/main.go | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 QuickStart.md diff --git a/QuickStart.md b/QuickStart.md new file mode 100644 index 0000000..bd70a6b --- /dev/null +++ b/QuickStart.md @@ -0,0 +1,65 @@ +# Quick Start + +Here's a short walk through of setting up and using the tokenizer proxy. +The use case here is running the proxy from a public fly address, such that it +is accessible by any other fly app that has a valid wrapped secret. + +## Config file + +The config file I used is called `fly.toml.timkenizer` with the following contents: + +``` +app = 'timkenizer' +primary_region = 'sjc' +kill_signal = 'SIGINT' + +[build] + +[env] + OPEN_PROXY = 'false' + REQUIRE_FLY_SRC = 'true' + TOKENIZER_HOSTNAMES = 'timkenizer.fly.dev' + +[http_service] + internal_port = 8080 + auto_stop_machines = 'off' + auto_start_machines = false + min_machines_running = 1 + processes = ['app'] + +[[vm]] + memory = '2gb' + cpu_kind = 'shared' + cpus = 1 +``` + +## Commands + +The commands I used to create the app and use it are: + +``` +# create the app, it will fail to start +fly -c fly.toml.timkenizer launch + +# generate and set the secret key +export OPEN_KEY=$(openssl rand -hex 32) +fly -c fly.toml.timkenizer secrets set OPEN_KEY=$OPEN_KEY + +# find the seal key in the logs +fly -c fly.toml.timkenizer logs -n |grep -o 'seal_key=.*' +seal_key=xxxxxx +export SEAL_KEY=$seal_key + +# seal a token, here restricted to use against https://timflyio-go-example.fly.dev from app=thenewsh +TOKEN=$(go run ./cmd/sealtoken -host timflyio-go-example.fly.dev -org tim-newsham -app thenewsh MY_SECRET_TOKEN) + +# and use it from the approved app to the approved url +# note: you'll need to opt-in to get a fly-src header to approve the request. +curl -H "Proxy-Tokenizer: $TOKEN" -H "fly-src-optin: *" -x https://timkenizer.fly.dev http://timflyio-go-example.fly.dev + +# try out some bad requests to the wrong target, from the wrong app, etc.. +curl -H "Proxy-Tokenizer: $TOKEN" -H "fly-src-optin: *" -x https://timkenizer.fly.dev http://thenewsh.fly.dev + +# review the log files +fly -c fly.toml.timkenizer logs +``` diff --git a/cmd/tokenizer/main.go b/cmd/tokenizer/main.go index af755c8..732ccc2 100644 --- a/cmd/tokenizer/main.go +++ b/cmd/tokenizer/main.go @@ -75,7 +75,7 @@ func runServe() { key := os.Getenv("OPEN_KEY") if key == "" { - fmt.Fprintf(os.Stderr, "missing OPEN_KEY") + fmt.Fprintf(os.Stderr, "missing OPEN_KEY\n") os.Exit(1) } From 16132fc241888ef7d08c385ba02fa04b0592a932 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Wed, 8 Oct 2025 13:48:22 -1000 Subject: [PATCH 07/11] remove TODO --- tokenizer.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tokenizer.go b/tokenizer.go index a0cb970..d8cab16 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -260,8 +260,6 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht pud.reqLog = logrus.WithFields(reqLogFields(ctx.Req)) } - // XXX TODO: will fly src headers appear in each request within the proxy stream? - // XXX or do we have to get the fly src once for the connect message and share? fsrc, err := flysrc.FromRequest(req) if err != nil { if t.RequireFlySrc { From a08e0279c7248c5040d617404d509139e4b3a7e8 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Wed, 8 Oct 2025 13:58:22 -1000 Subject: [PATCH 08/11] include a flag to print the seal key --- QuickStart.md | 17 ++++++++--------- cmd/tokenizer/main.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/QuickStart.md b/QuickStart.md index bd70a6b..864ea18 100644 --- a/QuickStart.md +++ b/QuickStart.md @@ -41,20 +41,19 @@ The commands I used to create the app and use it are: # create the app, it will fail to start fly -c fly.toml.timkenizer launch -# generate and set the secret key +# generate and set the secret "open" and "seal" keys. +# install the OPEN_KEY on the server and keep the SEAL_KEY for later. export OPEN_KEY=$(openssl rand -hex 32) +export SEAL_KEY=$(go run ./cmd/tokenizer -sealkey) fly -c fly.toml.timkenizer secrets set OPEN_KEY=$OPEN_KEY -# find the seal key in the logs -fly -c fly.toml.timkenizer logs -n |grep -o 'seal_key=.*' -seal_key=xxxxxx -export SEAL_KEY=$seal_key - -# seal a token, here restricted to use against https://timflyio-go-example.fly.dev from app=thenewsh +# use the SEAL_KEY to generate a proxy token that will inject a secret token into requests to the target. +# here restricted to use against https://timflyio-go-example.fly.dev from app=thenewsh TOKEN=$(go run ./cmd/sealtoken -host timflyio-go-example.fly.dev -org tim-newsham -app thenewsh MY_SECRET_TOKEN) -# and use it from the approved app to the approved url -# note: you'll need to opt-in to get a fly-src header to approve the request. +# install the TOKEN in your approved app and use it to access the approved url. +# the secret token (MY_SECRET_TOKEN) will be added as a bearer token. +# note: you'll need to opt-in to get a fly-src header to allow the proxy to approve the request. curl -H "Proxy-Tokenizer: $TOKEN" -H "fly-src-optin: *" -x https://timkenizer.fly.dev http://timflyio-go-example.fly.dev # try out some bad requests to the wrong target, from the wrong app, etc.. diff --git a/cmd/tokenizer/main.go b/cmd/tokenizer/main.go index 732ccc2..27056e3 100644 --- a/cmd/tokenizer/main.go +++ b/cmd/tokenizer/main.go @@ -32,6 +32,7 @@ var ( var ( versionFlag = flag.Bool("version", false, "print the version number") + sealKeyFlag = flag.Bool("sealkey", false, "print the seal key and exit") ) func init() { @@ -57,6 +58,8 @@ func main() { switch { case *versionFlag: runVersion() + case *sealKeyFlag: + runSealKey() default: runServe() } @@ -141,6 +144,17 @@ func handleSignals(server *http.Server) { } } +func runSealKey() { + key := os.Getenv("OPEN_KEY") + if key == "" { + fmt.Fprintf(os.Stderr, "missing OPEN_KEY\n") + os.Exit(1) + } + + tkz := tokenizer.NewTokenizer(key) + fmt.Fprintf(os.Stderr, "export SEAL_KEY=%v\n", tkz.SealKey()) +} + var Version = "" func runVersion() { From 7666b81ef449fe4db2e2e922ad4d14216a8d1a00 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Thu, 9 Oct 2025 15:43:28 -1000 Subject: [PATCH 09/11] get rid of flysrc internal library and instead use public superfly/flysrc-go library. Since the flysrc parser used to be static, we now have to pass in a context to the authorizers so they can get the flysrc parser instance that was constructed for the tokenizer instance. --- authorizer.go | 21 ++++-- flysrc/context.go | 20 ----- flysrc/fly_src.go | 168 ----------------------------------------- flysrc/fly_src_test.go | 65 ---------------- go.mod | 7 +- go.sum | 13 ++-- tokenizer.go | 39 +++++++--- tokenizer_test.go | 38 +++++++--- 8 files changed, 80 insertions(+), 291 deletions(-) delete mode 100644 flysrc/context.go delete mode 100644 flysrc/fly_src.go delete mode 100644 flysrc/fly_src_test.go diff --git a/authorizer.go b/authorizer.go index 84e897e..237ebb5 100644 --- a/authorizer.go +++ b/authorizer.go @@ -13,11 +13,11 @@ import ( "time" "github.com/sirupsen/logrus" + "github.com/superfly/flysrc-go" "github.com/superfly/macaroon" "github.com/superfly/macaroon/bundle" "github.com/superfly/macaroon/flyio" "github.com/superfly/macaroon/flyio/machinesapi" - "github.com/superfly/tokenizer/flysrc" tkmac "github.com/superfly/tokenizer/macaroon" "golang.org/x/exp/slices" ) @@ -35,8 +35,12 @@ func init() { redactedBase64, _ = base64.StdEncoding.DecodeString("REDACTED") } +type AuthContext interface { + GetFlysrcParser() *flysrc.Parser +} + type AuthConfig interface { - AuthRequest(req *http.Request) error + AuthRequest(authctx AuthContext, req *http.Request) error StripHazmat() AuthConfig } @@ -107,7 +111,7 @@ func NewBearerAuthConfig(token string) *BearerAuthConfig { var _ AuthConfig = (*BearerAuthConfig)(nil) -func (c *BearerAuthConfig) AuthRequest(req *http.Request) error { +func (c *BearerAuthConfig) AuthRequest(authctx AuthContext, req *http.Request) error { for _, tok := range proxyAuthorizationTokens(req) { hdrDigest := sha256.Sum256([]byte(tok)) if subtle.ConstantTimeCompare(c.Digest, hdrDigest[:]) == 1 { @@ -132,7 +136,7 @@ func NewMacaroonAuthConfig(key []byte) *MacaroonAuthConfig { var _ AuthConfig = (*MacaroonAuthConfig)(nil) -func (c *MacaroonAuthConfig) AuthRequest(req *http.Request) error { +func (c *MacaroonAuthConfig) AuthRequest(authctx AuthContext, req *http.Request) error { var ( expectedKID = tkmac.KeyFingerprint(c.Key) log = logrus.WithField("expected-kid", hex.EncodeToString(expectedKID)) @@ -194,7 +198,7 @@ func NewFlyioMacaroonAuthConfig(access *flyio.Access) *FlyioMacaroonAuthConfig { var _ AuthConfig = (*FlyioMacaroonAuthConfig)(nil) -func (c *FlyioMacaroonAuthConfig) AuthRequest(req *http.Request) error { +func (c *FlyioMacaroonAuthConfig) AuthRequest(authctx AuthContext, req *http.Request) error { var ctx = req.Context() for _, tok := range proxyAuthorizationTokens(req) { @@ -279,8 +283,9 @@ func NewFlySrcAuthConfig(opts ...FlySrcOpt) *FlySrcAuthConfig { var _ AuthConfig = (*FlySrcAuthConfig)(nil) -func (c *FlySrcAuthConfig) AuthRequest(req *http.Request) error { - fs, err := flysrc.FromRequest(req) +func (c *FlySrcAuthConfig) AuthRequest(authctx AuthContext, req *http.Request) error { + flysrcParser := authctx.GetFlysrcParser() + fs, err := flysrcParser.FromRequest(req) if err != nil { return fmt.Errorf("%w: %w", ErrNotAuthorized, err) } @@ -308,7 +313,7 @@ type NoAuthConfig struct{} var _ AuthConfig = (*NoAuthConfig)(nil) -func (c *NoAuthConfig) AuthRequest(req *http.Request) error { +func (c *NoAuthConfig) AuthRequest(authctx AuthContext, req *http.Request) error { return nil } diff --git a/flysrc/context.go b/flysrc/context.go deleted file mode 100644 index 16f2ce7..0000000 --- a/flysrc/context.go +++ /dev/null @@ -1,20 +0,0 @@ -package flysrc - -import ( - "context" -) - -type contextKey string - -const ( - contextKeyFlySrc = contextKey("fly-src") -) - -func WithFlySrc(ctx context.Context, fsrc *Parsed) context.Context { - return context.WithValue(ctx, contextKeyFlySrc, fsrc) -} - -func FlySrcFromContext(ctx context.Context) *Parsed { - fsrc, _ := ctx.Value(contextKeyFlySrc).(*Parsed) - return fsrc -} diff --git a/flysrc/fly_src.go b/flysrc/fly_src.go deleted file mode 100644 index 2ead851..0000000 --- a/flysrc/fly_src.go +++ /dev/null @@ -1,168 +0,0 @@ -package flysrc - -import ( - "crypto/ed25519" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "net" - "net/http" - "os" - "strconv" - "strings" - "time" - - "github.com/sirupsen/logrus" -) - -const ( - headerFlySrc = "Fly-Src" - headerFlySrcSignature = "Fly-Src-Signature" - flySrcSignatureKeyPath = "/.fly/fly-src.pub" - - maxFlySrcAge = 30 * time.Second -) - -var VerifyKey = readFlySrcKey(flySrcSignatureKeyPath) - -var FlyProxyNet = net.IPNet{ - IP: net.ParseIP("172.16.0.0"), - Mask: net.CIDRMask(16, 32), -} - -type Parsed struct { - Org string - App string - Instance string - Timestamp time.Time -} - -func FromRequest(req *http.Request) (*Parsed, error) { - if p := FlySrcFromContext(req.Context()); p != nil { - return p, nil - } - - srcHdr := req.Header.Get(headerFlySrc) - if srcHdr == "" { - return nil, errors.New("missing Fly-Src header") - } - - sigHdr := req.Header.Get(headerFlySrcSignature) - if sigHdr == "" { - return nil, errors.New("missing Fly-Src signature") - } - - host, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - return nil, errors.New("fly-src from malformed remote address") - } - - peerIp := net.ParseIP(host) - if peerIp == nil { - return nil, errors.New("fly-src from malformed ip") - } - - // The fly-src is only trusted if it comes from fly-proxy, which - // means it will arrive to the service_ip (127.19.x.x/16) from 172.16.x.x/16. - if !FlyProxyNet.Contains(peerIp) { - return nil, errors.New("fly-src is not from fly-proxy") - } - - return verifyAndParseFlySrc(srcHdr, sigHdr, VerifyKey) -} - -func verifyAndParseFlySrc(srcHdr, sigHdr string, key ed25519.PublicKey) (*Parsed, error) { - sig, err := base64.StdEncoding.DecodeString(sigHdr) - if err != nil { - return nil, fmt.Errorf("bad Fly-Src signature: %w", err) - } - - if !ed25519.Verify(key, []byte(srcHdr), sig) { - return nil, errors.New("bad Fly-Src signature") - } - - p, err := parseFlySrc(srcHdr) - if err != nil { - return nil, fmt.Errorf("bad Fly-Src header: %w", err) - } - - if p.age() > maxFlySrcAge { - return nil, fmt.Errorf("expired Fly-Src header") - } - - return p, nil -} - -func parseFlySrc(hdr string) (*Parsed, error) { - var ret Parsed - - parts := strings.Split(hdr, ";") - if n := len(parts); n != 4 { - return nil, fmt.Errorf("malformed Fly-Src header (%d parts)", n) - } - - for _, part := range parts { - k, v, ok := strings.Cut(part, "=") - if !ok { - return nil, fmt.Errorf("malformed Fly-Src header (missing =)") - } - - switch k { - case "org": - ret.Org = v - case "app": - ret.App = v - case "instance": - ret.Instance = v - case "ts": - tsi, err := strconv.Atoi(v) - if err != nil { - return nil, fmt.Errorf("malformed Fly-Src timestamp: %w", err) - } - - ret.Timestamp = time.Unix(int64(tsi), 0) - default: - return nil, fmt.Errorf("malformed Fly-Src header (unknown key: %q)", k) - } - } - - if ret.Org == "" || ret.App == "" || ret.Instance == "" || ret.Timestamp.IsZero() { - return nil, fmt.Errorf("malformed Fly-Src header (missing parts)") - } - - return &ret, nil -} - -func (p *Parsed) String() string { - return fmt.Sprintf("instance=%s;app=%s;org=%s;ts=%d", p.Instance, p.App, p.Org, p.Timestamp.Unix()) -} - -func (p *Parsed) Sign(key ed25519.PrivateKey) string { - return base64.StdEncoding.EncodeToString(ed25519.Sign(key, []byte(p.String()))) -} - -func (p *Parsed) age() time.Duration { - return time.Since(p.Timestamp) -} - -func readFlySrcKey(path string) ed25519.PublicKey { - hk, err := os.ReadFile(path) - if err != nil { - logrus.WithError(err).Warn("failed to read Fly-Src public key") - return nil - } - - if size := len(hk); hex.DecodedLen(size) != ed25519.PublicKeySize { - logrus.WithField("size", size).Warn("bad Fly-Src public key size") - return nil - } - - key := make(ed25519.PublicKey, ed25519.PublicKeySize) - if _, err := hex.Decode(key, hk); err != nil { - logrus.WithError(err).Warn("bad Fly-Src public key") - return nil - } - - return key -} diff --git a/flysrc/fly_src_test.go b/flysrc/fly_src_test.go deleted file mode 100644 index 4d0b052..0000000 --- a/flysrc/fly_src_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package flysrc - -import ( - "crypto/ed25519" - "os" - "path/filepath" - "testing" - "time" - - "github.com/alecthomas/assert/v2" -) - -func TestVerifyAndParseFlySrc(t *testing.T) { - pub, priv, err := ed25519.GenerateKey(nil) - assert.NoError(t, err) - - // good - p := &Parsed{"foo", "bar", "baz", time.Now().Truncate(time.Second)} - fs2, err := verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.NoError(t, err) - assert.Equal(t, p, fs2) - - // expired - p = &Parsed{"foo", "bar", "baz", time.Now().Add(-time.Hour).Truncate(time.Second)} - _, err = verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.Error(t, err) - - // bad signature - p = &Parsed{"foo", "bar", "baz", time.Now().Truncate(time.Second)} - sig := (&Parsed{"other", "bar", "baz", time.Now().Truncate(time.Second)}).Sign(priv) - _, err = verifyAndParseFlySrc(p.String(), sig, pub) - assert.Error(t, err) - - // missing fields - p = &Parsed{"", "bar", "baz", time.Now().Truncate(time.Second)} - _, err = verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.Error(t, err) - - p = &Parsed{"foo", "", "baz", time.Now().Truncate(time.Second)} - _, err = verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.Error(t, err) - - p = &Parsed{"foo", "bar", "", time.Now().Truncate(time.Second)} - _, err = verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.Error(t, err) - - p = &Parsed{"foo", "bar", "baz", time.Time{}} - _, err = verifyAndParseFlySrc(p.String(), p.Sign(priv), pub) - assert.Error(t, err) - - // totally bogus - _, err = verifyAndParseFlySrc("hello world!", sig, pub) - assert.Error(t, err) -} - -func TestReadFlySrcKey(t *testing.T) { - var ( - path = filepath.Join(t.TempDir(), "k.pub") - keyHex = "93e9adb1615a6ce6238a13c264e7c8ba8f8b7a53717e86bb34fce3b80d45f1e5" - key = []byte{147, 233, 173, 177, 97, 90, 108, 230, 35, 138, 19, 194, 100, 231, 200, 186, 143, 139, 122, 83, 113, 126, 134, 187, 52, 252, 227, 184, 13, 69, 241, 229} - ) - - assert.NoError(t, os.WriteFile(path, []byte(keyHex), 0644)) - assert.Equal(t, key, readFlySrcKey(path)) -} diff --git a/go.mod b/go.mod index df18cc0..310cccc 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,20 @@ module github.com/superfly/tokenizer -go 1.20 +go 1.22.6 require ( - github.com/alecthomas/assert/v2 v2.3.0 + github.com/alecthomas/assert/v2 v2.11.0 github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 github.com/sirupsen/logrus v1.9.3 + github.com/superfly/flysrc-go v0.0.2 github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ) require ( - github.com/alecthomas/repr v0.2.0 // indirect + github.com/alecthomas/repr v0.4.0 // indirect github.com/aws/smithy-go v1.20.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index 496e1f2..8b10313 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= -github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= @@ -28,8 +28,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/superfly/macaroon v0.2.14-0.20240718172852-139f90b76537 h1:xL2tIkau3Dr3dd4WOLbGz14kRcF49x15bVIMdOkLTyI= -github.com/superfly/macaroon v0.2.14-0.20240718172852-139f90b76537/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/superfly/flysrc-go v0.0.2 h1:/XKz8h7rkk6nuX76cb6YedA3dG3V4yP0N6opUrkrcLU= +github.com/superfly/flysrc-go v0.0.2/go.mod h1:xChBoolsYRcpANrrdDMQc7u8mERwWmQLu+vu7Cmdg/A= github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 h1:YQG1v1QcTFQxJureNBcbtxosZ98u78ceUNCDQgI/vgM= github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= diff --git a/tokenizer.go b/tokenizer.go index d8cab16..465ee41 100644 --- a/tokenizer.go +++ b/tokenizer.go @@ -24,7 +24,7 @@ import ( "golang.org/x/crypto/nacl/box" "golang.org/x/exp/maps" - "github.com/superfly/tokenizer/flysrc" + "github.com/superfly/flysrc-go" ) var FilteredHeaders = []string{headerProxyAuthorization, headerProxyTokenizer} @@ -60,6 +60,12 @@ type tokenizer struct { priv *[32]byte pub *[32]byte + + flysrcParser *flysrc.Parser +} + +func (t *tokenizer) GetFlysrcParser() *flysrc.Parser { + return t.flysrcParser } type Option func(*tokenizer) @@ -78,6 +84,14 @@ func RequireFlySrc() Option { } } +// WithFlysrcParser specifies a preconfigured flysrc parser to use instead of the +// default flysrc parser. +func WithFlysrcParser(parser *flysrc.Parser) Option { + return func(t *tokenizer) { + t.flysrcParser = parser + } +} + // TokenizerHostnames is a list of hostnames where tokenizer can be reached. If // provided, this allows tokenizer to transparently proxy requests (ie. accept // normal HTTP requests with arbitrary hostnames) while blocking circular @@ -102,12 +116,20 @@ func NewTokenizer(openKey string, opts ...Option) *tokenizer { curve25519.ScalarBaseMult(pub, priv) proxy := goproxy.NewProxyHttpServer() - tkz := &tokenizer{ProxyHttpServer: proxy, priv: priv, pub: pub} + tkz := &tokenizer{ProxyHttpServer: proxy, priv: priv, pub: pub, flysrcParser: nil} for _, opt := range opts { opt(tkz) } + if tkz.flysrcParser == nil { + parser, err := flysrc.New() + if err != nil { + logrus.WithError(err).Panic("Error making flysrc parser") + } + tkz.flysrcParser = parser + } + hostnameMap := map[string]bool{} for _, hostname := range tkz.tokenizerHostnames { hostnameMap[hostname] = true @@ -260,19 +282,18 @@ func (t *tokenizer) HandleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*ht pud.reqLog = logrus.WithFields(reqLogFields(ctx.Req)) } - fsrc, err := flysrc.FromRequest(req) + src, err := t.flysrcParser.FromRequest(req) if err != nil { if t.RequireFlySrc { pud.reqLog.Warn(err.Error()) return nil, errorResponse(ErrBadRequest) } } else { - req = req.Clone(flysrc.WithFlySrc(req.Context(), fsrc)) pud.reqLog = pud.reqLog.WithFields(logrus.Fields{ - "flysrc-org": fsrc.Org, - "flysrc-app": fsrc.App, - "flysrc-instance": fsrc.Instance, - "flysrc-timestamp": fsrc.Timestamp, + "flysrc-org": src.Org, + "flysrc-app": src.App, + "flysrc-instance": src.Instance, + "flysrc-timestamp": src.Timestamp, }) } @@ -377,7 +398,7 @@ func (t *tokenizer) processorsFromRequest(req *http.Request) ([]RequestProcessor } safeSecret = secret.StripHazmatString() - if err = secret.AuthRequest(req); err != nil { + if err = secret.AuthRequest(t, req); err != nil { return nil, safeSecret, fmt.Errorf("unauthorized to use secret: %w", err) } diff --git a/tokenizer_test.go b/tokenizer_test.go index 2f268bc..778a06e 100644 --- a/tokenizer_test.go +++ b/tokenizer_test.go @@ -22,16 +22,13 @@ import ( "github.com/alecthomas/assert/v2" "github.com/sirupsen/logrus" + "github.com/superfly/flysrc-go" "github.com/superfly/macaroon" - "github.com/superfly/tokenizer/flysrc" tkmac "github.com/superfly/tokenizer/macaroon" "golang.org/x/crypto/nacl/box" ) func TestTokenizer(t *testing.T) { - // honor fly-src when from 127.0.0.1/16 instead of from 172.16.0.0/16. - flysrc.FlyProxyNet.IP = net.ParseIP("127.0.0.1") - appServer := httptest.NewTLSServer(echo) defer appServer.Close() @@ -48,7 +45,17 @@ func TestTokenizer(t *testing.T) { openKey = hex.EncodeToString(priv[:]) ) - tkz := NewTokenizer(openKey) + flysrcParser, err := flysrc.New( + flysrc.WithPubkey(flySrcVerifyKey(t)), + flysrc.WithFlyProxyNet(&net.IPNet{ + IP: net.ParseIP("127.0.0.1"), + Mask: net.CIDRMask(16, 32), + }), + ) + + assert.NoError(t, err) + + tkz := NewTokenizer(openKey, WithFlysrcParser(flysrcParser)) tkz.ProxyHttpServer.Verbose = true tkzServer := httptest.NewServer(tkz) @@ -391,7 +398,7 @@ func TestTokenizer(t *testing.T) { assert.NoError(t, err) // Good - fs := &flysrc.Parsed{Org: "foo", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} + fs := &flysrc.FlySrc{Org: "foo", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} hdrSrc := fs.String() hdrSig := fs.Sign(priv) @@ -411,7 +418,7 @@ func TestTokenizer(t *testing.T) { }, doEcho(t, client, req)) // Bad org - fs = &flysrc.Parsed{Org: "WRONG!", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} + fs = &flysrc.FlySrc{Org: "WRONG!", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} hdrSrc = fs.String() hdrSig = fs.Sign(priv) @@ -427,7 +434,7 @@ func TestTokenizer(t *testing.T) { assert.Equal(t, http.StatusProxyAuthRequired, resp.StatusCode) // Missing signature - fs = &flysrc.Parsed{Org: "foo", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} + fs = &flysrc.FlySrc{Org: "foo", App: "bar", Instance: "baz", Timestamp: time.Now().Truncate(time.Second)} hdrSrc = fs.String() assert.NoError(t, err) @@ -528,7 +535,8 @@ func hmacSHA256(t testing.TB, key []byte, msg string) []byte { var ( _setupFlySrcSignKey sync.Once - _flySrcsignKey ed25519.PrivateKey + _flySrcSignKey ed25519.PrivateKey + _flySrcVerifyKey ed25519.PublicKey ) func flySrcSignKey(t *testing.T) ed25519.PrivateKey { @@ -537,10 +545,16 @@ func flySrcSignKey(t *testing.T) ed25519.PrivateKey { var err error _setupFlySrcSignKey.Do(func() { - flysrc.VerifyKey, _flySrcsignKey, err = ed25519.GenerateKey(nil) + _flySrcVerifyKey, _flySrcSignKey, err = ed25519.GenerateKey(nil) }) assert.NoError(t, err) - assert.NotZero(t, _flySrcsignKey) - return _flySrcsignKey + assert.NotZero(t, _flySrcSignKey) + return _flySrcSignKey +} + +func flySrcVerifyKey(t *testing.T) ed25519.PublicKey { + t.Helper() + _ = flySrcSignKey(t) + return _flySrcVerifyKey } From ce9ea89e6a40975fcf2a7cf1119212c33057f1f8 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Wed, 15 Oct 2025 09:01:34 -1000 Subject: [PATCH 10/11] bump dep version --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 310cccc..a4c03f2 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 github.com/sirupsen/logrus v1.9.3 - github.com/superfly/flysrc-go v0.0.2 + github.com/superfly/flysrc-go v0.0.3 github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 diff --git a/go.sum b/go.sum index 8b10313..0142fea 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/superfly/flysrc-go v0.0.2 h1:/XKz8h7rkk6nuX76cb6YedA3dG3V4yP0N6opUrkrcLU= github.com/superfly/flysrc-go v0.0.2/go.mod h1:xChBoolsYRcpANrrdDMQc7u8mERwWmQLu+vu7Cmdg/A= +github.com/superfly/flysrc-go v0.0.3 h1:ykFxTVWX9sp6s+9ukg5ZQWbco0wT3TOiWwODhR3Q4UI= +github.com/superfly/flysrc-go v0.0.3/go.mod h1:xChBoolsYRcpANrrdDMQc7u8mERwWmQLu+vu7Cmdg/A= github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648 h1:YQG1v1QcTFQxJureNBcbtxosZ98u78ceUNCDQgI/vgM= github.com/superfly/macaroon v0.2.14-0.20240819201738-61a02aa53648/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= From 1b4f537db6da53712e0b33ba0994cc56772d2de6 Mon Sep 17 00:00:00 2001 From: Tim Newsham Date: Wed, 15 Oct 2025 09:08:53 -1000 Subject: [PATCH 11/11] fix dockerfile for latest changes --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4eed6e5..8d67de7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine AS builder +FROM golang:1.22-alpine AS builder WORKDIR /go/src/github.com/superfly/tokenizer COPY go.mod go.sum ./ @@ -9,7 +9,6 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ COPY VERSION ./ COPY *.go ./ COPY ./macaroon ./macaroon -COPY ./flysrc ./flysrc COPY ./cmd/tokenizer ./cmd/tokenizer RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg \