From 9bee6bb648e5180cd43d96aa6f23106965bc7d75 Mon Sep 17 00:00:00 2001 From: Bobby DeSimone Date: Tue, 24 Mar 2020 20:23:07 -0700 Subject: [PATCH] cmd: add cli to generate service accounts (#552) Signed-off-by: Bobby DeSimone --- cmd/pomerium-cli/cli.go | 169 ++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 3 + 3 files changed, 173 insertions(+) create mode 100644 cmd/pomerium-cli/cli.go diff --git a/cmd/pomerium-cli/cli.go b/cmd/pomerium-cli/cli.go new file mode 100644 index 000000000..90512cece --- /dev/null +++ b/cmd/pomerium-cli/cli.go @@ -0,0 +1,169 @@ +package main + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "errors" + "flag" + "fmt" + "os" + "strings" + "time" + + "github.com/fatih/color" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +type stringSlice []string + +func (i *stringSlice) String() string { + return fmt.Sprint(*i) +} + +func (i *stringSlice) Set(value string) error { + if len(*i) > 0 { + return errors.New("already set") + } + for _, dt := range strings.Split(value, ",") { + *i = append(*i, dt) + } + return nil +} + +type serviceAccount struct { + // Standard claims (as specified in RFC 7519). + jwt.Claims + // Pomerium claims (not standard claims) + Email string `json:"email"` + Groups []string `json:"groups,omitempty"` + User string `json:"user,omitempty"` + ImpersonateEmail string `json:"impersonate_email,omitempty"` + ImpersonateGroups []string `json:"impersonate_groups,omitempty"` +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, color.RedString("\n⛔️ %s\n\n"), err) + printHelp(flags) + os.Exit(1) + } + os.Exit(0) +} + +var flags *flag.FlagSet + +func run() error { + var sa serviceAccount + + // temporary variables we will use to hydrate our service account + // struct from basic types pulled in from our flags + var ( + aud stringSlice + groups stringSlice + impersonateGroups stringSlice + expiry time.Duration + ) + + // set our JWT claims from the supplied CLI flags + flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + flags.StringVar(&sa.Email, "email", "", "Email") + flags.StringVar(&sa.ImpersonateEmail, "impersonate_email", "", "Impersonation Email (optional)") + flags.StringVar(&sa.Issuer, "iss", "", "Issuing Server (e.g authenticate.int.pomerium.io)") + flags.StringVar(&sa.Subject, "sub", "", "Subject (typically User's GUID)") + flags.StringVar(&sa.User, "user", "", "User (typically User's GUID)") + flags.Var(&aud, "aud", "Audience (e.g. httpbin.int.pomerium.io,prometheus.int.pomerium.io)") + flags.Var(&groups, "groups", "Groups (e.g. admins@pomerium.io,users@pomerium.io)") + flags.Var(&impersonateGroups, "impersonate_groups", "Impersonation Groups (optional)") + flags.DurationVar(&expiry, "expiry", time.Hour, "Expiry") + + // hydrate the sessions non-primate types + if err := flags.Parse(os.Args[1:]); err != nil { + return err + } + sa.Audience = jwt.Audience(aud) + sa.Groups = []string(groups) + sa.ImpersonateGroups = []string(impersonateGroups) + sa.Expiry = jwt.NewNumericDate(time.Now().Add(expiry)) + sa.IssuedAt = jwt.NewNumericDate(time.Now()) + sa.NotBefore = jwt.NewNumericDate(time.Now()) + // why not be pretty? + c := color.New(color.FgGreen) + // check that we've got our shared key to sign our jwt + var sharedKey string + args := flags.Args() + if len(args) == 1 { + sharedKey = args[0] + } else { + if _, err := c.Println("Enter base64 encoded shared key >"); err != nil { + return err + } + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + sharedKey = scanner.Text() + } + + if sharedKey == "" { + return errors.New("shared key required") + } + + if sa.Email == "" { + return errors.New("email is required") + } + + if len(sa.Audience) == 0 { + return errors.New("aud is required") + } + + if sa.Issuer == "" { + return errors.New("iss is required") + } + + decodedKey, err := base64.StdEncoding.DecodeString(sharedKey) + if err != nil { + return fmt.Errorf("shared key not base64: %w", err) + } + + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: decodedKey}, nil) + if err != nil { + return fmt.Errorf("bad shared key: %w", err) + } + raw, err := jwt.Signed(signer).Claims(sa).CompactSerialize() + if err != nil { + return fmt.Errorf("couldn't sign jwt: %w", err) + } + saJSON, err := json.MarshalIndent(sa, "", " ") + if err != nil { + return fmt.Errorf("couldn't pretty print jwt: %w", err) + } + if _, err := c.Println("Service Account"); err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n\n", saJSON) + if _, err := c.Println("JWT 🍪"); err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n\n", raw) + + return nil +} + +func printHelp(fs *flag.FlagSet) { + fmt.Fprintf(os.Stderr, strings.TrimSpace(help)+"\n\n", os.Args[0]) + fs.PrintDefaults() +} + +const help = ` +pomerium-sa generates a pomerium service account from a shared key. + +Usage: %[1]s [flags] [base64'd shared secret setting] + +For additional help see: + + https://www.pomerium.io + https://jwt.io/ + +Flags: + +` diff --git a/go.mod b/go.mod index ea8e458e9..599d04ad8 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( contrib.go.opencensus.io/exporter/prometheus v0.1.0 github.com/cespare/xxhash/v2 v2.1.1 github.com/coreos/go-oidc v2.2.1+incompatible + github.com/fatih/color v1.7.0 github.com/fsnotify/fsnotify v1.4.9 github.com/go-redis/redis/v7 v7.2.0 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e diff --git a/go.sum b/go.sum index 2b5a00da9..7ccb45278 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -193,7 +194,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39 h1:0E3wlIAcvD6zt/8UJgTd4JMT6UQhsnYyjCIqllyVLbs= github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=