github.com/grailbio/base@v0.0.11/cmd/grail-access/main.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 // The following enables go generate to generate the doc.go file. 6 //go:generate go run v.io/x/lib/cmdline/gendoc "--build-cmd=go install" --copyright-notice= . -help 7 8 package main 9 10 import ( 11 "fmt" 12 "os" 13 "strings" 14 "time" 15 16 "github.com/grailbio/base/cmd/grail-access/remote" 17 "github.com/grailbio/base/errors" 18 "github.com/grailbio/base/log" 19 _ "github.com/grailbio/v23/factories/grail" 20 v23 "v.io/v23" 21 "v.io/v23/context" 22 "v.io/v23/security" 23 "v.io/x/lib/cmdline" 24 "v.io/x/ref" 25 libsecurity "v.io/x/ref/lib/security" 26 ) 27 28 const ( 29 // DATA(sensitive): These are the OAuth2 client ID and secret. They were 30 // generated in the grail-razvanm Google Cloud Project. The client secret 31 // is not secret in this case because it is part of client tool. It does act 32 // as an identifier that allows restriction based on quota on the Google 33 // side. 34 clientID = "fake" 35 clientSecret = "fake" 36 ) 37 38 var ( 39 credentialsDirFlag string 40 41 blesserFlag string 42 browserFlag bool 43 googleOauth2Flag string 44 ec2Flag bool 45 ec2InstanceIdentityFlag string 46 k8sFlag bool 47 regionFlag string 48 caCrtFlag string 49 namespaceFlag string 50 tokenFlag string 51 52 dumpFlag bool 53 doNotRefreshDurationFlag time.Duration 54 expiryCaveatFlag string 55 56 blessRemotesFlag bool 57 blessRemotesModeFlag string 58 blessRemotesTargetsFlag FlagStrings 59 ) 60 61 func init() { 62 blessRemotesTargetsFlag = []string{os.ExpandEnv("ec2-name:ubuntu@adhoc.${USER}.*")} 63 } 64 65 func main() { 66 var defaultCredentialsDir string 67 if dir, ok := os.LookupEnv(ref.EnvCredentials); ok { 68 defaultCredentialsDir = dir 69 } else { 70 // TODO(josh): This expands to /.v23 if $HOME is undefined. 71 // We keep this for backwards compatibility, but maybe we shouldn't. 72 defaultCredentialsDir = os.ExpandEnv("${HOME}/.v23") 73 } 74 75 cmd := &cmdline.Command{ 76 Runner: cmdline.RunnerFunc(run), 77 Name: "grail-access", 78 Short: "Creates fresh Vanadium credentials", 79 Long: ` 80 Command grail-access creates Vanadium credentials (also called principals) using 81 either Google ID tokens (the default) or the AWS IAM role attached to an EC2 82 instance (requested using the '-ec2' flag). 83 84 For the Google-based auth the user will be prompted to go through an 85 OAuth flow that requires minimal permissions (only 'Know who you are 86 on Google') and obtains an ID token scoped to the clientID expected by 87 the server. The ID token is presented to the server via a Vanadium 88 RPC. For a 'xxx@grailbio.com' email address the server will hand to 89 the client a '[server]:google:xxx@grailbio.com' blessing where 90 '[server]' is the blessing of the server. 91 92 For the EC2-based auth an instance with ID 'i-0aec7b085f8432699' in the account 93 number '619867110810' using the 'adhoc' role the server will hand to the client 94 a '[server]:ec2:619867110810:role:adhoc:i-0aec7b085f8432699' blessing where 95 'server' is the blessing of the server. 96 `, 97 } 98 cmd.Flags.StringVar(&credentialsDirFlag, "dir", defaultCredentialsDir, "Where to store the Vanadium credentials. NOTE: the content will be erased if the credentials are regenerated.") 99 cmd.Flags.StringVar(&blesserFlag, "blesser", "", "Flow specific blesser endpoint to use. Defaults to /ticket-server.eng.grail.com:8102/blesser/<flow>.") 100 cmd.Flags.BoolVar(&browserFlag, "browser", os.Getenv("SSH_CLIENT") == "", "Attempt to open a browser.") 101 cmd.Flags.StringVar(&googleOauth2Flag, "google-oauth2-url", 102 "https://accounts.google.com/o/oauth2", 103 "URL for oauth2 API calls, for testing") 104 cmd.Flags.BoolVar(&ec2Flag, "ec2", false, "Use the role of the EC2 VM.") 105 cmd.Flags.StringVar(&ec2InstanceIdentityFlag, "ec2-instance-identity-url", 106 "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7", 107 "URL for fetching instance identity document, for testing") 108 cmd.Flags.BoolVar(&k8sFlag, "k8s", false, "Use the Kubernetes flow.") 109 cmd.Flags.StringVar(®ionFlag, "region", "us-west-2", "AWS EKS region to use for k8s cluster token review.") 110 cmd.Flags.StringVar(&caCrtFlag, "ca-crt", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "Path to ca.crt file.") 111 cmd.Flags.StringVar(&namespaceFlag, "namespace", "/var/run/secrets/kubernetes.io/serviceaccount/namespace", "Path to namespace file.") 112 cmd.Flags.StringVar(&tokenFlag, "token", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to token file.") 113 cmd.Flags.BoolVar(&dumpFlag, "dump", false, "If credentials are present, dump them on the console instead of refreshing them.") 114 cmd.Flags.DurationVar(&doNotRefreshDurationFlag, "do-not-refresh-duration", 7*24*time.Hour, "Do not refresh credentials if they are present and do not expire within this duration.") 115 cmd.Flags.StringVar(&expiryCaveatFlag, "expiry-caveat", "", "Duration of expiry caveat added to blessings (for testing); empty means no caveat added") 116 117 // TODO(2022-10-18): Fix commentary generation to bring doc.go up to date. 118 // go.mod is currently broken such that required go tooling fails. We are 119 // apparently specifying old versions of protobuf related packages, which 120 // causes `go install` to fail, which causes doc generation to fail. 121 cmd.Flags.BoolVar(&blessRemotesFlag, "bless-remotes", true, "Whether to attempt to bless remotes with local blessings; only applies to Google blessings") 122 cmd.Flags.StringVar(&blessRemotesModeFlag, remote.FlagNameMode, "", "(INTERNAL) Controls the mode in which we run for the remote blessing protocol; one of {public-key,receive,send}") 123 cmd.Flags.Var(&blessRemotesTargetsFlag, "bless-remotes-targets", "Comma-separated list of targets to bless; targets may be \"ssh:[user@]host[:port]\" SSH destinations or \"ec2-name:[user@]ec2-instance-name-filter\" EC2 instance name filters; see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html") 124 125 cmdline.HideGlobalFlagsExcept() 126 cmdline.Main(cmd) 127 } 128 129 func run(*cmdline.Env, []string) error { 130 if credentialsDirFlag == "" { 131 return fmt.Errorf("missing credentials dir, need -dir, $HOME, or $%s", ref.EnvCredentials) 132 } 133 134 if _, ok := os.LookupEnv(ref.EnvCredentials); !ok { 135 fmt.Print("*******************************************************\n") 136 fmt.Printf("* WARNING: $%s is not defined! *\n", ref.EnvCredentials) 137 fmt.Printf("*******************************************************\n\n") 138 fmt.Printf("How to fix this in bash: export %s=%s\n\n", ref.EnvCredentials, credentialsDirFlag) 139 } 140 principal, err := libsecurity.LoadPersistentPrincipal(credentialsDirFlag, nil) 141 if err != nil { 142 log.Printf("INFO: Couldn't load principal from %s. Creating new one...", credentialsDirFlag) 143 _, createErr := libsecurity.CreatePersistentPrincipal(credentialsDirFlag, nil) 144 if createErr != nil { 145 return errors.E(fmt.Sprintf("failed to create new principal: %v, after load error: %v", createErr, err)) 146 } 147 principal, err = libsecurity.LoadPersistentPrincipal(credentialsDirFlag, nil) 148 } 149 if err != nil { 150 return errors.E("failed to load principal", err) 151 } 152 153 ctx, shutDown := v23.Init() 154 defer shutDown() 155 ctx, err = v23.WithPrincipal(ctx, principal) 156 if err != nil { 157 return errors.E("failed to initialize context", err) 158 } 159 switch blessRemotesModeFlag { 160 case "": 161 // No-op. 162 case remote.ModePublicKey: 163 if err = remote.PrintPublicKey(ctx, os.Stdout); err != nil { 164 return errors.E("failed to print public key", err) 165 } 166 return nil 167 case remote.ModeReceive: 168 if err = remote.ReceiveBlessings(ctx, os.Stdin); err != nil { 169 return errors.E("failed to receive blessings", err) 170 } 171 return nil 172 default: 173 return errors.E("invalid -"+remote.FlagNameMode, blessRemotesModeFlag) 174 } 175 defaultBlessings, _ := principal.BlessingStore().Default() 176 if dumpFlag || defaultBlessings.Expiry().After(time.Now().Add(doNotRefreshDurationFlag)) { 177 dump(principal) 178 if err = maybeBlessRemotes(ctx); err != nil { 179 return err 180 } 181 return nil 182 } 183 184 var blessings security.Blessings 185 if ec2Flag { 186 blessings, err = fetchEC2Blessings(ctx) 187 } else if k8sFlag { 188 blessings, err = fetchK8sBlessings(ctx) 189 } else { 190 blessings, err = fetchGoogleBlessings(ctx) 191 } 192 if err != nil { 193 return errors.E("failed to fetch blessings", err) 194 } 195 if expiryCaveatFlag != "" { 196 d, err := time.ParseDuration(expiryCaveatFlag) 197 if err != nil { 198 return errors.E("failed to parse expiry-caveat") 199 } 200 expiryCaveat, err := security.NewExpiryCaveat(time.Now().Add(d)) 201 if err != nil { 202 return errors.E("failed to make expiry caveat", err) 203 } 204 extension := fmt.Sprintf("expires-%v", d) 205 blessings, err = principal.Bless(principal.PublicKey(), blessings, extension, expiryCaveat) 206 if err != nil { 207 return errors.E("failed to make expired blessings", err) 208 } 209 } 210 if err = principal.BlessingStore().SetDefault(blessings); err != nil { 211 return errors.E(err, "failed to set default blessings") 212 } 213 _, err = principal.BlessingStore().Set(blessings, security.AllPrincipals) 214 if err != nil { 215 return errors.E(err, "failed to set peer blessings") 216 } 217 if err := security.AddToRoots(principal, blessings); err != nil { 218 return errors.E(err, "failed to add blessing roots") 219 } 220 221 fmt.Println("Successfully applied new blessing:") 222 dump(principal) 223 if err = maybeBlessRemotes(ctx); err != nil { 224 return err 225 } 226 return nil 227 } 228 229 func dump(principal security.Principal) { 230 // Mimic the output of the v.io/x/ref/cmd/principal dump command. 231 fmt.Printf("Public key: %s\n", principal.PublicKey()) 232 fmt.Println("---------------- BlessingStore ----------------") 233 fmt.Print(principal.BlessingStore().DebugString()) 234 fmt.Println("---------------- BlessingRoots ----------------") 235 fmt.Print(principal.Roots().DebugString()) 236 237 blessing, _ := principal.BlessingStore().Default() 238 fmt.Printf("Expires on %s (in %s)\n", blessing.Expiry().Local(), time.Until(blessing.Expiry())) 239 } 240 241 func maybeBlessRemotes(ctx *context.T) error { 242 if !blessRemotesFlag { 243 return nil 244 } 245 // The only use case for blessing remotes is for Google blessings, i.e. 246 // using the local browser for OAuth to bless a headless EC2 instance. 247 const prefix = "v23.grail.com:google:" 248 blessings, _ := v23.GetPrincipal(ctx).BlessingStore().Default() 249 if !strings.HasPrefix(blessings.String(), prefix) { 250 return nil 251 } 252 if err := remote.Bless(ctx, blessRemotesTargetsFlag); err != nil { 253 return errors.E("failed to send blessings to instances", err) 254 } 255 return nil 256 }