Skip to content

Commit 0969c5d

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

File tree

22 files changed

+273
-552
lines changed

22 files changed

+273
-552
lines changed

cli/server.go

Lines changed: 9 additions & 81 deletions
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

Lines changed: 16 additions & 10 deletions
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

Lines changed: 0 additions & 5 deletions
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

Lines changed: 38 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 10 additions & 5 deletions
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

Lines changed: 2 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy