feat: enable key rotation (#15066)

This PR contains the remaining logic necessary to hook up key rotation
to the product.
This commit is contained in:
Jon Ayers
2024-10-25 17:14:35 +01:00
committed by GitHub
parent ccfffc6911
commit cd890aa3a0
54 changed files with 1412 additions and 1129 deletions

View File

@ -40,6 +40,7 @@ import (
"github.com/coder/quartz"
"github.com/coder/serpent"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/entitlements"
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/runtimeconfig"
@ -185,9 +186,6 @@ type Options struct {
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
// workspace applications. It consists of both a signing and encryption key.
AppSecurityKey workspaceapps.SecurityKey
// CoordinatorResumeTokenProvider is used to provide and validate resume
// tokens issued by and passed to the coordinator DRPC API.
CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider
@ -251,6 +249,12 @@ type Options struct {
// OneTimePasscodeValidityPeriod specifies how long a one time passcode should be valid for.
OneTimePasscodeValidityPeriod time.Duration
// Keycaches
AppSigningKeyCache cryptokeys.SigningKeycache
AppEncryptionKeyCache cryptokeys.EncryptionKeycache
OIDCConvertKeyCache cryptokeys.SigningKeycache
Clock quartz.Clock
}
// @title Coder API
@ -352,6 +356,9 @@ func New(options *Options) *API {
if options.PrometheusRegistry == nil {
options.PrometheusRegistry = prometheus.NewRegistry()
}
if options.Clock == nil {
options.Clock = quartz.NewReal()
}
if options.DERPServer == nil && options.DeploymentValues.DERP.Server.Enable {
options.DERPServer = derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp")))
}
@ -444,6 +451,49 @@ func New(options *Options) *API {
if err != nil {
panic(xerrors.Errorf("get deployment ID: %w", err))
}
fetcher := &cryptokeys.DBFetcher{
DB: options.Database,
}
if options.OIDCConvertKeyCache == nil {
options.OIDCConvertKeyCache, err = cryptokeys.NewSigningCache(ctx,
options.Logger.Named("oidc_convert_keycache"),
fetcher,
codersdk.CryptoKeyFeatureOIDCConvert,
)
if err != nil {
options.Logger.Critical(ctx, "failed to properly instantiate oidc convert signing cache", slog.Error(err))
}
}
if options.AppSigningKeyCache == nil {
options.AppSigningKeyCache, err = cryptokeys.NewSigningCache(ctx,
options.Logger.Named("app_signing_keycache"),
fetcher,
codersdk.CryptoKeyFeatureWorkspaceAppsToken,
)
if err != nil {
options.Logger.Critical(ctx, "failed to properly instantiate app signing key cache", slog.Error(err))
}
}
if options.AppEncryptionKeyCache == nil {
options.AppEncryptionKeyCache, err = cryptokeys.NewEncryptionCache(ctx,
options.Logger,
fetcher,
codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey,
)
if err != nil {
options.Logger.Critical(ctx, "failed to properly instantiate app encryption key cache", slog.Error(err))
}
}
// Start a background process that rotates keys. We intentionally start this after the caches
// are created to force initial requests for a key to populate the caches. This helps catch
// bugs that may only occur when a key isn't precached in tests and the latency cost is minimal.
cryptokeys.StartRotator(ctx, options.Logger, options.Database)
api := &API{
ctx: ctx,
cancel: cancel,
@ -464,7 +514,7 @@ func New(options *Options) *API {
options.DeploymentValues,
oauthConfigs,
options.AgentInactiveDisconnectTimeout,
options.AppSecurityKey,
options.AppSigningKeyCache,
),
metricsCache: metricsCache,
Auditor: atomic.Pointer[audit.Auditor]{},
@ -606,7 +656,7 @@ func New(options *Options) *API {
ResumeTokenProvider: api.Options.CoordinatorResumeTokenProvider,
})
if err != nil {
api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err))
api.Logger.Fatal(context.Background(), "failed to initialize tailnet client service", slog.Error(err))
}
api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{
@ -628,9 +678,6 @@ func New(options *Options) *API {
options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter
}
if options.AppSecurityKey.IsZero() {
api.Logger.Fatal(api.ctx, "app security key cannot be zero")
}
api.workspaceAppServer = &workspaceapps.Server{
Logger: workspaceAppsLogger,
@ -642,11 +689,11 @@ func New(options *Options) *API {
SignedTokenProvider: api.WorkspaceAppsProvider,
AgentProvider: api.agentProvider,
AppSecurityKey: options.AppSecurityKey,
StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions),
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
APIKeyEncryptionKeycache: options.AppEncryptionKeyCache,
}
apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
@ -1434,6 +1481,9 @@ func (api *API) Close() error {
_ = api.agentProvider.Close()
_ = api.statsReporter.Close()
_ = api.NetworkTelemetryBatcher.Close()
_ = api.OIDCConvertKeyCache.Close()
_ = api.AppSigningKeyCache.Close()
_ = api.AppEncryptionKeyCache.Close()
return nil
}