Skip to content

Commit 0969c5d

Browse files
committed
the end is nigh
1 parent 5d8490a commit 0969c5d

22 files changed

+273
-552
lines changed

cli/server.go

+9-81
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"crypto/tls"
1111
"crypto/x509"
1212
"database/sql"
13-
"encoding/hex"
1413
"errors"
1514
"flag"
1615
"fmt"
@@ -62,6 +61,7 @@ import (
6261
"github.com/coder/serpent"
6362
"github.com/coder/wgtunnel/tunnelsdk"
6463

64+
"github.com/coder/coder/v2/coderd/cryptokeys"
6565
"github.com/coder/coder/v2/coderd/entitlements"
6666
"github.com/coder/coder/v2/coderd/notifications/reports"
6767
"github.com/coder/coder/v2/coderd/runtimeconfig"
@@ -97,7 +97,6 @@ import (
9797
"github.com/coder/coder/v2/coderd/updatecheck"
9898
"github.com/coder/coder/v2/coderd/util/slice"
9999
stringutil "github.com/coder/coder/v2/coderd/util/strings"
100-
"github.com/coder/coder/v2/coderd/workspaceapps"
101100
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
102101
"github.com/coder/coder/v2/coderd/workspacestats"
103102
"github.com/coder/coder/v2/codersdk"
@@ -741,90 +740,19 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
741740
return xerrors.Errorf("set deployment id: %w", err)
742741
}
743742
}
744-
745-
// Read the app signing key from the DB. We store it hex encoded
746-
// since the config table uses strings for the value and we
747-
// don't want to deal with automatic encoding issues.
748-
appSecurityKeyStr, err := tx.GetAppSecurityKey(ctx)
749-
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
750-
return xerrors.Errorf("get app signing key: %w", err)
751-
}
752-
// If the string in the DB is an invalid hex string or the
753-
// length is not equal to the current key length, generate a new
754-
// one.
755-
//
756-
// If the key is regenerated, old signed tokens and encrypted
757-
// strings will become invalid. New signed app tokens will be
758-
// generated automatically on failure. Any workspace app token
759-
// smuggling operations in progress may fail, although with a
760-
// helpful error.
761-
if decoded, err := hex.DecodeString(appSecurityKeyStr); err != nil || len(decoded) != len(workspaceapps.SecurityKey{}) {
762-
b := make([]byte, len(workspaceapps.SecurityKey{}))
763-
_, err := rand.Read(b)
764-
if err != nil {
765-
return xerrors.Errorf("generate fresh app signing key: %w", err)
766-
}
767-
768-
appSecurityKeyStr = hex.EncodeToString(b)
769-
err = tx.UpsertAppSecurityKey(ctx, appSecurityKeyStr)
770-
if err != nil {
771-
return xerrors.Errorf("insert freshly generated app signing key to database: %w", err)
772-
}
773-
}
774-
775-
appSecurityKey, err := workspaceapps.KeyFromString(appSecurityKeyStr)
776-
if err != nil {
777-
return xerrors.Errorf("decode app signing key from database: %w", err)
778-
}
779-
780-
options.AppSecurityKey = appSecurityKey
781-
782-
// Read the oauth signing key from the database. Like the app security, generate a new one
783-
// if it is invalid for any reason.
784-
oauthSigningKeyStr, err := tx.GetOAuthSigningKey(ctx)
785-
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
786-
return xerrors.Errorf("get app oauth signing key: %w", err)
787-
}
788-
if decoded, err := hex.DecodeString(oauthSigningKeyStr); err != nil || len(decoded) != len(options.OAuthSigningKey) {
789-
b := make([]byte, len(options.OAuthSigningKey))
790-
_, err := rand.Read(b)
791-
if err != nil {
792-
return xerrors.Errorf("generate fresh oauth signing key: %w", err)
793-
}
794-
795-
oauthSigningKeyStr = hex.EncodeToString(b)
796-
err = tx.UpsertOAuthSigningKey(ctx, oauthSigningKeyStr)
797-
if err != nil {
798-
return xerrors.Errorf("insert freshly generated oauth signing key to database: %w", err)
799-
}
800-
}
801-
802-
oauthKeyBytes, err := hex.DecodeString(oauthSigningKeyStr)
803-
if err != nil {
804-
return xerrors.Errorf("decode oauth signing key from database: %w", err)
805-
}
806-
if len(oauthKeyBytes) != len(options.OAuthSigningKey) {
807-
return xerrors.Errorf("oauth signing key in database is not the correct length, expect %d got %d", len(options.OAuthSigningKey), len(oauthKeyBytes))
808-
}
809-
copy(options.OAuthSigningKey[:], oauthKeyBytes)
810-
if options.OAuthSigningKey == [32]byte{} {
811-
return xerrors.Errorf("oauth signing key in database is empty")
812-
}
813-
814-
// Read the coordinator resume token signing key from the
815-
// database.
816-
resumeTokenKey, err := tailnet.ResumeTokenSigningKeyFromDatabase(ctx, tx)
817-
if err != nil {
818-
return xerrors.Errorf("get coordinator resume token key from database: %w", err)
819-
}
820-
options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeTokenKey, quartz.NewReal(), tailnet.DefaultResumeTokenExpiry)
821-
822743
return nil
823744
}, nil)
824745
if err != nil {
825-
return err
746+
return xerrors.Errorf("set deployment id: %w", err)
826747
}
827748

749+
resumeKeycache, err := cryptokeys.NewSigningCache(logger, options.Database, database.CryptoKeyFeatureTailnetResume)
750+
if err != nil {
751+
return xerrors.Errorf("create resume token key cache: %w", err)
752+
}
753+
754+
options.CoordinatorResumeTokenProvider = tailnet.NewResumeTokenKeyProvider(resumeKeycache, quartz.NewReal(), tailnet.DefaultResumeTokenExpiry)
755+
828756
options.RuntimeConfig = runtimeconfig.NewManager()
829757

830758
// This should be output before the logs start streaming.

coderd/coderd.go

+16-10
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,7 @@ type Options struct {
186186
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
187187
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
188188
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
189-
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
190189
// workspace applications. It consists of both a signing and encryption key.
191-
AppSecurityKey workspaceapps.SecurityKey
192190
// CoordinatorResumeTokenProvider is used to provide and validate resume
193191
// tokens issued by and passed to the coordinator DRPC API.
194192
CoordinatorResumeTokenProvider tailnet.ResumeTokenProvider
@@ -445,6 +443,14 @@ func New(options *Options) *API {
445443
if err != nil {
446444
panic(xerrors.Errorf("get deployment ID: %w", err))
447445
}
446+
appSigningKeyCache, err := cryptokeys.NewSigningCache(options.Logger.Named("app_signing_key_cache"), options.Database, database.CryptoKeyFeatureWorkspaceAppsToken)
447+
if err != nil {
448+
options.Logger.Fatal(ctx, "failed to initialize app signing key cache", slog.Error(err))
449+
}
450+
appEncryptingKeyCache, err := cryptokeys.NewEncryptionCache(options.Logger.Named("app_encrypting_key_cache"), options.Database, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
451+
if err != nil {
452+
options.Logger.Fatal(ctx, "failed to initialize app encrypting key cache", slog.Error(err))
453+
}
448454
api := &API{
449455
ctx: ctx,
450456
cancel: cancel,
@@ -465,7 +471,7 @@ func New(options *Options) *API {
465471
options.DeploymentValues,
466472
oauthConfigs,
467473
options.AgentInactiveDisconnectTimeout,
468-
options.AppSecurityKey,
474+
appSigningKeyCache,
469475
),
470476
metricsCache: metricsCache,
471477
Auditor: atomic.Pointer[audit.Auditor]{},
@@ -620,6 +626,7 @@ func New(options *Options) *API {
620626
if err != nil {
621627
api.Logger.Fatal(api.ctx, "failed to initialize oauth convert key cache", slog.Error(err))
622628
}
629+
api.workspaceAppsKeyCache = appEncryptingKeyCache
623630

624631
api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{
625632
Database: options.Database,
@@ -640,9 +647,6 @@ func New(options *Options) *API {
640647
options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter
641648
}
642649

643-
if options.AppSecurityKey.IsZero() {
644-
api.Logger.Fatal(api.ctx, "app security key cannot be zero")
645-
}
646650
api.workspaceAppServer = &workspaceapps.Server{
647651
Logger: workspaceAppsLogger,
648652

@@ -654,11 +658,12 @@ func New(options *Options) *API {
654658

655659
SignedTokenProvider: api.WorkspaceAppsProvider,
656660
AgentProvider: api.agentProvider,
657-
AppSecurityKey: options.AppSecurityKey,
658661
StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions),
659662

660-
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
661-
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
663+
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
664+
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
665+
Signer: appSigningKeyCache,
666+
EncryptingKeyManager: appEncryptingKeyCache,
662667
}
663668

664669
apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
@@ -1405,7 +1410,8 @@ type API struct {
14051410
// resumeTokenKeycache is used to fetch and cache keys used for signing JWTs
14061411
// oauthConvertKeycache is used to fetch and cache keys used for signing JWTs
14071412
// during OAuth conversions. See userauth.go.convertUserToOauth.
1408-
oauthConvertKeycache cryptokeys.SigningKeycache
1413+
oauthConvertKeycache cryptokeys.SigningKeycache
1414+
workspaceAppsKeyCache cryptokeys.EncryptionKeycache
14091415
}
14101416

14111417
// Close waits for all WebSocket connections to drain before returning.

coderd/coderdtest/coderdtest.go

-5
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ import (
9090
"github.com/coder/coder/v2/testutil"
9191
)
9292

93-
// AppSecurityKey is a 96-byte key used to sign JWTs and encrypt JWEs for
94-
// workspace app tokens in tests.
95-
var AppSecurityKey = must(workspaceapps.KeyFromString("6465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e2077617320686572"))
96-
9793
type Options struct {
9894
// AccessURL denotes a custom access URL. By default we use the httptest
9995
// server's URL. Setting this may result in unexpected behavior (especially
@@ -525,7 +521,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
525521
DeploymentOptions: codersdk.DeploymentOptionsWithoutSecrets(options.DeploymentValues.Options()),
526522
UpdateCheckOptions: options.UpdateCheckOptions,
527523
SwaggerEndpoint: options.SwaggerEndpoint,
528-
AppSecurityKey: AppSecurityKey,
529524
SSHConfig: options.ConfigSSH,
530525
HealthcheckFunc: options.HealthcheckFunc,
531526
HealthcheckTimeout: options.HealthcheckTimeout,

coderd/jwtutils/jwe.go

+38
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const (
1515
encryptContentAlgo = jose.A256GCM
1616
)
1717

18+
type EncryptingKeyManager interface {
19+
EncryptKeyProvider
20+
DecryptKeyProvider
21+
}
22+
1823
type EncryptKeyProvider interface {
1924
EncryptingKey(ctx context.Context) (id string, key interface{}, err error)
2025
}
@@ -65,6 +70,12 @@ func Encrypt(ctx context.Context, e EncryptKeyProvider, claims Claims) (string,
6570
return compact, nil
6671
}
6772

73+
func WithDecryptExpected(expected jwt.Expected) func(*DecryptOptions) {
74+
return func(opts *DecryptOptions) {
75+
opts.RegisteredClaims = expected
76+
}
77+
}
78+
6879
// DecryptOptions are options for decrypting a JWE.
6980
type DecryptOptions struct {
7081
RegisteredClaims jwt.Expected
@@ -119,3 +130,30 @@ func Decrypt(ctx context.Context, d DecryptKeyProvider, token string, claims Cla
119130

120131
return claims.Validate(options.RegisteredClaims)
121132
}
133+
134+
type StaticKeyManager struct {
135+
ID string
136+
Key interface{}
137+
}
138+
139+
func (s StaticKeyManager) SigningKey(_ context.Context) (string, interface{}, error) {
140+
return s.ID, s.Key, nil
141+
}
142+
143+
func (s StaticKeyManager) VerifyingKey(_ context.Context, id string) (interface{}, error) {
144+
if id != s.ID {
145+
return nil, xerrors.Errorf("invalid id %q", id)
146+
}
147+
return s.Key, nil
148+
}
149+
150+
func (s StaticKeyManager) EncryptingKey(_ context.Context) (string, interface{}, error) {
151+
return s.ID, s.Key, nil
152+
}
153+
154+
func (s StaticKeyManager) DecryptingKey(_ context.Context, id string) (interface{}, error) {
155+
if id != s.ID {
156+
return nil, xerrors.Errorf("invalid id %q", id)
157+
}
158+
return s.Key, nil
159+
}

coderd/jwtutils/jws.go

+27
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@ const (
2424
signingAlgo = jose.HS512
2525
)
2626

27+
type StaticKeyManager struct {
28+
ID string
29+
Key interface{}
30+
}
31+
32+
func (s StaticKeyManager) SigningKey(_ context.Context) (string, interface{}, error) {
33+
return s.ID, s.Key, nil
34+
}
35+
36+
func (s StaticKeyManager) VerifyingKey(_ context.Context, id string) (interface{}, error) {
37+
if id != s.ID {
38+
return nil, xerrors.Errorf("invalid id %q", id)
39+
}
40+
return s.Key, nil
41+
}
42+
43+
type SigningKeyManager interface {
44+
SigningKeyProvider
45+
VerifyKeyProvider
46+
}
47+
2748
type SigningKeyProvider interface {
2849
SigningKey(ctx context.Context) (id string, key interface{}, err error)
2950
}
@@ -75,6 +96,12 @@ type VerifyOptions struct {
7596
SignatureAlgorithm jose.SignatureAlgorithm
7697
}
7798

99+
func WithVerifyExpected(expected jwt.Expected) func(*VerifyOptions) {
100+
return func(opts *VerifyOptions) {
101+
opts.RegisteredClaims = expected
102+
}
103+
}
104+
78105
// Verify verifies that a token was signed by the provided key. It unmarshals into the provided claims.
79106
func Verify(ctx context.Context, v VerifyKeyProvider, token string, claims Claims, opts ...func(*VerifyOptions)) error {
80107
options := VerifyOptions{

coderd/workspaceagents.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
853853
)
854854
if resumeToken != "" {
855855
var err error
856-
peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(resumeToken)
856+
peerID, err = api.Options.CoordinatorResumeTokenProvider.VerifyResumeToken(ctx, resumeToken)
857857
if err != nil {
858858
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
859859
Message: workspacesdk.CoordinateAPIInvalidResumeToken,

coderd/workspaceagents_test.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/coder/coder/v2/coderd/database/dbtime"
3737
"github.com/coder/coder/v2/coderd/database/pubsub"
3838
"github.com/coder/coder/v2/coderd/externalauth"
39+
"github.com/coder/coder/v2/coderd/jwtutils"
3940
"github.com/coder/coder/v2/codersdk"
4041
"github.com/coder/coder/v2/codersdk/agentsdk"
4142
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -531,20 +532,20 @@ func newResumeTokenRecordingProvider(t testing.TB, underlying tailnet.ResumeToke
531532
}
532533
}
533534

534-
func (r *resumeTokenRecordingProvider) GenerateResumeToken(peerID uuid.UUID) (*tailnetproto.RefreshResumeTokenResponse, error) {
535+
func (r *resumeTokenRecordingProvider) GenerateResumeToken(ctx context.Context, peerID uuid.UUID) (*tailnetproto.RefreshResumeTokenResponse, error) {
535536
select {
536537
case r.generateCalls <- peerID:
537-
return r.ResumeTokenProvider.GenerateResumeToken(peerID)
538+
return r.ResumeTokenProvider.GenerateResumeToken(ctx, peerID)
538539
default:
539540
r.t.Error("generateCalls full")
540541
return nil, xerrors.New("generateCalls full")
541542
}
542543
}
543544

544-
func (r *resumeTokenRecordingProvider) VerifyResumeToken(token string) (uuid.UUID, error) {
545+
func (r *resumeTokenRecordingProvider) VerifyResumeToken(ctx context.Context, token string) (uuid.UUID, error) {
545546
select {
546547
case r.verifyCalls <- token:
547-
return r.ResumeTokenProvider.VerifyResumeToken(token)
548+
return r.ResumeTokenProvider.VerifyResumeToken(ctx, token)
548549
default:
549550
r.t.Error("verifyCalls full")
550551
return uuid.Nil, xerrors.New("verifyCalls full")
@@ -557,10 +558,14 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) {
557558
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
558559
clock := quartz.NewMock(t)
559560
resumeTokenSigningKey, err := tailnet.GenerateResumeTokenSigningKey()
561+
mgr := jwtutils.StaticKeyManager{
562+
ID: uuid.New().String(),
563+
Key: resumeTokenSigningKey,
564+
}
560565
require.NoError(t, err)
561566
resumeTokenProvider := newResumeTokenRecordingProvider(
562567
t,
563-
tailnet.NewResumeTokenKeyProvider(resumeTokenSigningKey, clock, time.Hour),
568+
tailnet.NewResumeTokenKeyProvider(mgr, clock, time.Hour),
564569
)
565570
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
566571
Coordinator: tailnet.NewCoordinator(logger),

coderd/workspaceapps.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/coder/v2/coderd/database/dbtime"
1717
"github.com/coder/coder/v2/coderd/httpapi"
1818
"github.com/coder/coder/v2/coderd/httpmw"
19+
"github.com/coder/coder/v2/coderd/jwtutils"
1920
"github.com/coder/coder/v2/coderd/rbac/policy"
2021
"github.com/coder/coder/v2/coderd/workspaceapps"
2122
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
@@ -122,8 +123,7 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request
122123
return
123124
}
124125

125-
// Encrypt the API key.
126-
encryptedAPIKey, err := api.AppSecurityKey.EncryptAPIKey(workspaceapps.EncryptedAPIKeyPayload{
126+
encryptedAPIKey, err := jwtutils.Encrypt(ctx, api.workspaceAppsKeyCache, workspaceapps.EncryptedAPIKeyPayload{
127127
APIKey: cookie.Value,
128128
})
129129
if err != nil {

0 commit comments

Comments
 (0)