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  }