go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/secrets/module.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package secrets 16 17 import ( 18 "context" 19 "flag" 20 21 secretmanager "cloud.google.com/go/secretmanager/apiv1" 22 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 23 "google.golang.org/api/option" 24 "google.golang.org/grpc" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/tsmon" 28 "go.chromium.org/luci/grpc/grpcmon" 29 30 "go.chromium.org/luci/server/auth" 31 "go.chromium.org/luci/server/module" 32 ) 33 34 // ModuleName can be used to refer to this module when declaring dependencies. 35 var ModuleName = module.RegisterName("go.chromium.org/luci/server/secrets") 36 37 // ModuleOptions contain configuration of the secrets server module. 38 type ModuleOptions struct { 39 // RootSecret points to the root secret used to derive random secrets. 40 // 41 // In production it should be a reference to a Google Secret Manager secret 42 // (in a form "sm://<project>/<secret>" or just "sm://<secret>" to fetch it 43 // from the current project). 44 // 45 // In non-production environments it can be a literal secret value in a form 46 // "devsecret://<base64-encoded secret>" or "devsecret-text://<secret>". If 47 // omitted in a non-production environment, some phony hardcoded value is 48 // used. 49 // 50 // When using Google Secret Manager, the secret version "latest" is used to 51 // get the current value of the root secret, and a single immediately 52 // preceding previous version (if it is still enabled) is used to get the 53 // previous version of the root secret. This allows graceful rotation of 54 // random secrets. 55 RootSecret string 56 57 // PrimaryTinkAEADKey is the secret name with the JSON-serialized clear text 58 // Tink AEAD keyset to use for AEAD operations by default via PrimaryTinkAEAD. 59 // 60 // It is optional. If unset, PrimaryTinkAEAD will return nil. Code that 61 // depends on a presence of an AEAD implementation must check that the return 62 // value of PrimaryTinkAEAD is not nil during startup. 63 PrimaryTinkAEADKey string 64 } 65 66 // Register registers the command line flags. 67 func (o *ModuleOptions) Register(f *flag.FlagSet) { 68 f.StringVar( 69 &o.RootSecret, 70 "root-secret", 71 o.RootSecret, 72 `Either "sm://<project>/<secret>" or "sm://<secret>" to use Google Secret Manager, `+ 73 `or "devsecret://<base64-encoded value>" or "devsecret-text://<value>" `+ 74 `for a static development secret.`, 75 ) 76 f.StringVar( 77 &o.PrimaryTinkAEADKey, 78 "primary-tink-aead-key", 79 o.PrimaryTinkAEADKey, 80 `A "sm://..." reference to a clear text JSON Tink AEAD key set to use for `+ 81 `AEAD operations by default. Optional, but some server modules may require `+ 82 `it and will refuse to start if it is not set. `+ 83 `For development, you need a valid AEAD keyset and pass it via `+ 84 `devsecret://... or devsecret-text://... or specify `+ 85 `devsecret-gen://tink/aead to automatically generate a new random key, `+ 86 `which you can then re-use via devsecret:// in the future.`, 87 ) 88 } 89 90 // NewModule returns a server module that adds a secret store backed by Google 91 // Secret Manager to the global server context. 92 func NewModule(opts *ModuleOptions) module.Module { 93 if opts == nil { 94 opts = &ModuleOptions{} 95 } 96 return &serverModule{opts: opts} 97 } 98 99 // NewModuleFromFlags is a variant of NewModule that initializes options through 100 // command line flags. 101 // 102 // Calling this function registers flags in flag.CommandLine. They are usually 103 // parsed in server.Main(...). 104 func NewModuleFromFlags() module.Module { 105 opts := &ModuleOptions{} 106 opts.Register(flag.CommandLine) 107 return NewModule(opts) 108 } 109 110 // serverModule implements module.Module. 111 type serverModule struct { 112 opts *ModuleOptions 113 } 114 115 // Name is part of module.Module interface. 116 func (*serverModule) Name() module.Name { 117 return ModuleName 118 } 119 120 // Dependencies is part of module.Module interface. 121 func (*serverModule) Dependencies() []module.Dependency { 122 return nil 123 } 124 125 // Initialize is part of module.Module interface. 126 func (m *serverModule) Initialize(ctx context.Context, host module.Host, opts module.HostOptions) (context.Context, error) { 127 if !opts.Prod && m.opts.RootSecret == "" { 128 m.opts.RootSecret = "devsecret-text://phony-root-secret-do-not-depend-on" 129 } 130 131 ts, err := auth.GetTokenSource(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 132 if err != nil { 133 return nil, errors.Annotate(err, "failed to initialize the token source").Err() 134 } 135 client, err := secretmanager.NewClient( 136 ctx, 137 option.WithTokenSource(ts), 138 option.WithGRPCDialOption(grpc.WithStatsHandler(&grpcmon.ClientRPCStatsMonitor{})), 139 option.WithGRPCDialOption(grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor())), 140 option.WithGRPCDialOption(grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor())), 141 ) 142 143 if err != nil { 144 return nil, errors.Annotate(err, "failed to initialize the Secret Manager client").Err() 145 } 146 host.RegisterCleanup(func(context.Context) { client.Close() }) 147 148 store := &SecretManagerStore{ 149 CloudProject: opts.CloudProject, 150 AccessSecretVersion: client.AccessSecretVersion, 151 } 152 ctx = Use(ctx, store) 153 154 if m.opts.RootSecret != "" { 155 if err := store.LoadRootSecret(ctx, m.opts.RootSecret); err != nil { 156 return nil, errors.Annotate(err, "failed to initialize the secret store").Err() 157 } 158 } 159 160 if m.opts.PrimaryTinkAEADKey != "" { 161 aead, err := LoadTinkAEAD(ctx, m.opts.PrimaryTinkAEADKey) 162 if err != nil { 163 return nil, errors.Annotate(err, "failed to initialize the primary tink AEAD key").Err() 164 } 165 ctx = setPrimaryTinkAEAD(ctx, aead) 166 } 167 168 host.RunInBackground("luci.secrets", store.MaintenanceLoop) 169 170 // Report initial values of metrics and refresh them on every tsmon flush. 171 store.ReportMetrics(ctx) 172 tsmon.RegisterCallbackIn(ctx, store.ReportMetrics) 173 174 return ctx, nil 175 }