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 1409 additions and 1126 deletions
+3 -4
View File
@@ -13,12 +13,11 @@ import (
var _ cryptokeys.Fetcher = &ProxyFetcher{}
type ProxyFetcher struct {
Client *wsproxysdk.Client
Feature codersdk.CryptoKeyFeature
Client *wsproxysdk.Client
}
func (p *ProxyFetcher) Fetch(ctx context.Context) ([]codersdk.CryptoKey, error) {
keys, err := p.Client.CryptoKeys(ctx)
func (p *ProxyFetcher) Fetch(ctx context.Context, feature codersdk.CryptoKeyFeature) ([]codersdk.CryptoKey, error) {
keys, err := p.Client.CryptoKeys(ctx, feature)
if err != nil {
return nil, xerrors.Errorf("crypto keys: %w", err)
}
+10 -6
View File
@@ -7,6 +7,8 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/jwtutils"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
)
@@ -18,18 +20,19 @@ type TokenProvider struct {
AccessURL *url.URL
AppHostname string
Client *wsproxysdk.Client
SecurityKey workspaceapps.SecurityKey
Logger slog.Logger
Client *wsproxysdk.Client
TokenSigningKeycache cryptokeys.SigningKeycache
APIKeyEncryptionKeycache cryptokeys.EncryptionKeycache
Logger slog.Logger
}
func (p *TokenProvider) FromRequest(r *http.Request) (*workspaceapps.SignedToken, bool) {
return workspaceapps.FromRequest(r, p.SecurityKey)
return workspaceapps.FromRequest(r, p.TokenSigningKeycache)
}
func (p *TokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *http.Request, issueReq workspaceapps.IssueTokenRequest) (*workspaceapps.SignedToken, string, bool) {
appReq := issueReq.AppRequest.Normalize()
err := appReq.Validate()
err := appReq.Check()
if err != nil {
workspaceapps.WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "invalid app request")
return nil, "", false
@@ -42,7 +45,8 @@ func (p *TokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *ht
}
// Check that it verifies properly and matches the string.
token, err := p.SecurityKey.VerifySignedToken(resp.SignedTokenStr)
var token workspaceapps.SignedToken
err = jwtutils.Verify(ctx, p.TokenSigningKeycache, resp.SignedTokenStr, &token)
if err != nil {
workspaceapps.WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "failed to verify newly generated signed token")
return nil, "", false
+54 -25
View File
@@ -31,6 +31,7 @@ import (
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/tracing"
@@ -130,6 +131,13 @@ type Server struct {
// the moon's token.
SDKClient *wsproxysdk.Client
// apiKeyEncryptionKeycache manages the encryption keys for smuggling API
// tokens to the alternate domain when using workspace apps.
apiKeyEncryptionKeycache cryptokeys.EncryptionKeycache
// appTokenSigningKeycache manages the signing keys for signing the app
// tokens we use for workspace apps.
appTokenSigningKeycache cryptokeys.SigningKeycache
// DERP
derpMesh *derpmesh.Mesh
derpMeshTLSConfig *tls.Config
@@ -195,19 +203,42 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(opts.Logger.Named("net.derp")))
ctx, cancel := context.WithCancel(context.Background())
encryptionCache, err := cryptokeys.NewEncryptionCache(ctx,
opts.Logger,
&ProxyFetcher{Client: client},
codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey,
)
if err != nil {
cancel()
return nil, xerrors.Errorf("create api key encryption cache: %w", err)
}
signingCache, err := cryptokeys.NewSigningCache(ctx,
opts.Logger,
&ProxyFetcher{Client: client},
codersdk.CryptoKeyFeatureWorkspaceAppsToken,
)
if err != nil {
cancel()
return nil, xerrors.Errorf("create api token signing cache: %w", err)
}
r := chi.NewRouter()
s := &Server{
Options: opts,
Handler: r,
DashboardURL: opts.DashboardURL,
Logger: opts.Logger.Named("net.workspace-proxy"),
TracerProvider: opts.Tracing,
PrometheusRegistry: opts.PrometheusRegistry,
SDKClient: client,
derpMesh: derpmesh.New(opts.Logger.Named("net.derpmesh"), derpServer, meshTLSConfig),
derpMeshTLSConfig: meshTLSConfig,
ctx: ctx,
cancel: cancel,
ctx: ctx,
cancel: cancel,
Options: opts,
Handler: r,
DashboardURL: opts.DashboardURL,
Logger: opts.Logger.Named("net.workspace-proxy"),
TracerProvider: opts.Tracing,
PrometheusRegistry: opts.PrometheusRegistry,
SDKClient: client,
derpMesh: derpmesh.New(opts.Logger.Named("net.derpmesh"), derpServer, meshTLSConfig),
derpMeshTLSConfig: meshTLSConfig,
apiKeyEncryptionKeycache: encryptionCache,
appTokenSigningKeycache: signingCache,
}
// Register the workspace proxy with the primary coderd instance and start a
@@ -240,11 +271,6 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
return nil, xerrors.Errorf("handle register: %w", err)
}
secKey, err := workspaceapps.KeyFromString(regResp.AppSecurityKey)
if err != nil {
return nil, xerrors.Errorf("parse app security key: %w", err)
}
agentProvider, err := coderd.NewServerTailnet(ctx,
s.Logger,
nil,
@@ -277,20 +303,21 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
HostnameRegex: opts.AppHostnameRegex,
RealIPConfig: opts.RealIPConfig,
SignedTokenProvider: &TokenProvider{
DashboardURL: opts.DashboardURL,
AccessURL: opts.AccessURL,
AppHostname: opts.AppHostname,
Client: client,
SecurityKey: secKey,
Logger: s.Logger.Named("proxy_token_provider"),
DashboardURL: opts.DashboardURL,
AccessURL: opts.AccessURL,
AppHostname: opts.AppHostname,
Client: client,
TokenSigningKeycache: signingCache,
APIKeyEncryptionKeycache: encryptionCache,
Logger: s.Logger.Named("proxy_token_provider"),
},
AppSecurityKey: secKey,
DisablePathApps: opts.DisablePathApps,
SecureAuthCookie: opts.SecureAuthCookie,
AgentProvider: agentProvider,
StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions),
AgentProvider: agentProvider,
StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions),
APIKeyEncryptionKeycache: encryptionCache,
}
derpHandler := derphttp.Handler(derpServer)
@@ -435,6 +462,8 @@ func (s *Server) Close() error {
err = multierror.Append(err, agentProviderErr)
}
s.SDKClient.SDKClient.HTTPClient.CloseIdleConnections()
_ = s.appTokenSigningKeycache.Close()
_ = s.apiKeyEncryptionKeycache.Close()
return err
}
+26
View File
@@ -25,6 +25,9 @@ import (
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/workspaceapps/apptest"
@@ -932,6 +935,9 @@ func TestWorkspaceProxyWorkspaceApps(t *testing.T) {
if opts.PrimaryAppHost == "" {
opts.PrimaryAppHost = "*.primary.test.coder.com"
}
db, pubsub := dbtestutil.NewDB(t)
client, closer, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: deploymentValues,
@@ -947,6 +953,8 @@ func TestWorkspaceProxyWorkspaceApps(t *testing.T) {
},
},
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
Database: db,
Pubsub: pubsub,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -959,6 +967,13 @@ func TestWorkspaceProxyWorkspaceApps(t *testing.T) {
_ = closer.Close()
})
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceAppsToken,
})
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
})
// Create the external proxy
if opts.DisableSubdomainApps {
opts.AppHost = ""
@@ -1002,6 +1017,8 @@ func TestWorkspaceProxyWorkspaceApps_BlockDirect(t *testing.T) {
if opts.PrimaryAppHost == "" {
opts.PrimaryAppHost = "*.primary.test.coder.com"
}
db, pubsub := dbtestutil.NewDB(t)
client, closer, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: deploymentValues,
@@ -1017,6 +1034,8 @@ func TestWorkspaceProxyWorkspaceApps_BlockDirect(t *testing.T) {
},
},
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
Database: db,
Pubsub: pubsub,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -1029,6 +1048,13 @@ func TestWorkspaceProxyWorkspaceApps_BlockDirect(t *testing.T) {
_ = closer.Close()
})
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceAppsToken,
})
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
})
// Create the external proxy
if opts.DisableSubdomainApps {
opts.AppHost = ""
+3 -10
View File
@@ -205,7 +205,6 @@ type RegisterWorkspaceProxyRequest struct {
}
type RegisterWorkspaceProxyResponse struct {
AppSecurityKey string `json:"app_security_key"`
DERPMeshKey string `json:"derp_mesh_key"`
DERPRegionID int32 `json:"derp_region_id"`
DERPMap *tailcfg.DERPMap `json:"derp_map"`
@@ -372,12 +371,6 @@ func (l *RegisterWorkspaceProxyLoop) Start(ctx context.Context) (RegisterWorkspa
}
failedAttempts = 0
// Check for consistency.
if originalRes.AppSecurityKey != resp.AppSecurityKey {
l.failureFn(xerrors.New("app security key has changed, proxy must be restarted"))
return
}
if originalRes.DERPMeshKey != resp.DERPMeshKey {
l.failureFn(xerrors.New("DERP mesh key has changed, proxy must be restarted"))
return
@@ -586,10 +579,10 @@ type CryptoKeysResponse struct {
CryptoKeys []codersdk.CryptoKey `json:"crypto_keys"`
}
func (c *Client) CryptoKeys(ctx context.Context) (CryptoKeysResponse, error) {
func (c *Client) CryptoKeys(ctx context.Context, feature codersdk.CryptoKeyFeature) (CryptoKeysResponse, error) {
res, err := c.Request(ctx, http.MethodGet,
"/api/v2/workspaceproxies/me/crypto-keys",
nil,
"/api/v2/workspaceproxies/me/crypto-keys", nil,
codersdk.WithQueryParam("feature", string(feature)),
)
if err != nil {
return CryptoKeysResponse{}, xerrors.Errorf("make request: %w", err)