Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 ./
Expand All @@ -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 \
Expand Down
64 changes: 64 additions & 0 deletions QuickStart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 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 "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

# 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)

# 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..
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
```
49 changes: 41 additions & 8 deletions authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -28,8 +28,20 @@ const (
maxFlySrcAge = 30 * time.Second
)

var redactedStr = "REDACTED"
var redactedBase64 []byte

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
}

type wireAuth struct {
Expand Down Expand Up @@ -99,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 {
Expand All @@ -110,6 +122,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"`
}
Expand All @@ -120,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))
Expand Down Expand Up @@ -150,6 +166,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 {
Expand Down Expand Up @@ -178,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) {
Expand All @@ -204,6 +224,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
Expand Down Expand Up @@ -259,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)
}
Expand All @@ -280,14 +305,22 @@ func (c *FlySrcAuthConfig) AuthRequest(req *http.Request) error {
return nil
}

func (c *FlySrcAuthConfig) StripHazmat() AuthConfig {
return c
}

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
}

func (c *NoAuthConfig) StripHazmat() AuthConfig {
return c
}

func proxyAuthorizationTokens(req *http.Request) (ret []string) {
hdrLoop:
for _, hdr := range req.Header.Values(headerProxyAuthorization) {
Expand Down
90 changes: 90 additions & 0 deletions cmd/sealtoken/main.go
Original file line number Diff line number Diff line change
@@ -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 <token>".
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)
}
}
20 changes: 19 additions & 1 deletion cmd/tokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -57,6 +58,8 @@ func main() {
switch {
case *versionFlag:
runVersion()
case *sealKeyFlag:
runSealKey()
default:
runServe()
}
Expand All @@ -75,7 +78,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)
}

Expand All @@ -89,6 +92,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 {
Expand Down Expand Up @@ -137,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() {
Expand Down
Loading