mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -65,6 +65,8 @@ type WorkspaceProxy struct {
|
||||
// owner client. If a token is provided, the proxy will become a replica of the
|
||||
// existing proxy region.
|
||||
func NewWorkspaceProxyReplica(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Client, options *ProxyOptions) WorkspaceProxy {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelFunc)
|
||||
|
||||
@@ -142,8 +144,10 @@ func NewWorkspaceProxyReplica(t *testing.T, coderdAPI *coderd.API, owner *coders
|
||||
statsCollectorOptions.Flush = options.FlushStats
|
||||
}
|
||||
|
||||
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug).With(slog.F("server_url", serverURL.String()))
|
||||
|
||||
wssrv, err := wsproxy.New(ctx, &wsproxy.Options{
|
||||
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug).With(slog.F("server_url", serverURL.String())),
|
||||
Logger: logger,
|
||||
Experiments: options.Experiments,
|
||||
DashboardURL: coderdAPI.AccessURL,
|
||||
AccessURL: accessURL,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,13 @@ import (
|
||||
"github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
|
||||
)
|
||||
|
||||
// whitelistedCryptoKeyFeatures is a list of crypto key features that are
|
||||
// allowed to be queried with workspace proxies.
|
||||
var whitelistedCryptoKeyFeatures = []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceAppsToken,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
}
|
||||
|
||||
// forceWorkspaceProxyHealthUpdate forces an update of the proxy health.
|
||||
// This is useful when a proxy is created or deleted. Errors will be logged.
|
||||
func (api *API) forceWorkspaceProxyHealthUpdate(ctx context.Context) {
|
||||
@@ -700,7 +708,6 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, wsproxysdk.RegisterWorkspaceProxyResponse{
|
||||
AppSecurityKey: api.AppSecurityKey.String(),
|
||||
DERPMeshKey: api.DERPServer.MeshKey(),
|
||||
DERPRegionID: regionID,
|
||||
DERPMap: api.AGPL.DERPMap(),
|
||||
@@ -721,13 +728,29 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param feature query string true "Feature key"
|
||||
// @Success 200 {object} wsproxysdk.CryptoKeysResponse
|
||||
// @Router /workspaceproxies/me/crypto-keys [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
func (api *API) workspaceProxyCryptoKeys(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
keys, err := api.Database.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
|
||||
feature := database.CryptoKeyFeature(r.URL.Query().Get("feature"))
|
||||
if feature == "" {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Missing feature query parameter.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Contains(whitelistedCryptoKeyFeatures, feature) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid feature: %q", feature),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := api.Database.GetCryptoKeysByFeature(ctx, feature)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
|
||||
@@ -320,7 +320,6 @@ func TestProxyRegisterDeregister(t *testing.T) {
|
||||
}
|
||||
registerRes1, err := proxyClient.RegisterWorkspaceProxy(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, registerRes1.AppSecurityKey)
|
||||
require.NotEmpty(t, registerRes1.DERPMeshKey)
|
||||
require.EqualValues(t, 10001, registerRes1.DERPRegionID)
|
||||
require.Empty(t, registerRes1.SiblingReplicas)
|
||||
@@ -609,11 +608,8 @@ func TestProxyRegisterDeregister(t *testing.T) {
|
||||
func TestIssueSignedAppToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
@@ -716,6 +712,10 @@ func TestReconnectingPTYSignedToken(t *testing.T) {
|
||||
closer.Close()
|
||||
})
|
||||
|
||||
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsToken,
|
||||
})
|
||||
|
||||
// Create a workspace + apps
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
@@ -915,51 +915,86 @@ func TestGetCryptoKeys(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
expectedKey1 := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-time.Hour),
|
||||
Sequence: 2,
|
||||
})
|
||||
key1 := db2sdk.CryptoKey(expectedKey1)
|
||||
encryptionKey := db2sdk.CryptoKey(expectedKey1)
|
||||
|
||||
expectedKey2 := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsToken,
|
||||
StartsAt: now,
|
||||
Sequence: 3,
|
||||
})
|
||||
key2 := db2sdk.CryptoKey(expectedKey2)
|
||||
signingKey := db2sdk.CryptoKey(expectedKey2)
|
||||
|
||||
// Create a deleted key.
|
||||
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-time.Hour),
|
||||
Secret: sql.NullString{
|
||||
String: "secret1",
|
||||
Valid: false,
|
||||
},
|
||||
Sequence: 1,
|
||||
})
|
||||
|
||||
// Create a key with different features.
|
||||
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureTailnetResume,
|
||||
StartsAt: now.Add(-time.Hour),
|
||||
Sequence: 1,
|
||||
})
|
||||
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureOidcConvert,
|
||||
StartsAt: now.Add(-time.Hour),
|
||||
Sequence: 1,
|
||||
Sequence: 4,
|
||||
})
|
||||
|
||||
proxy := coderdenttest.NewWorkspaceProxyReplica(t, api, cclient, &coderdenttest.ProxyOptions{
|
||||
Name: testutil.GetRandomName(t),
|
||||
})
|
||||
|
||||
keys, err := proxy.SDKClient.CryptoKeys(ctx)
|
||||
keys, err := proxy.SDKClient.CryptoKeys(ctx, codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, keys)
|
||||
// 1 key is generated on startup, the other we manually generated.
|
||||
require.Equal(t, 2, len(keys.CryptoKeys))
|
||||
requireContainsKeys(t, keys.CryptoKeys, key1, key2)
|
||||
requireContainsKeys(t, keys.CryptoKeys, encryptionKey)
|
||||
requireNotContainsKeys(t, keys.CryptoKeys, signingKey)
|
||||
|
||||
keys, err = proxy.SDKClient.CryptoKeys(ctx, codersdk.CryptoKeyFeatureWorkspaceAppsToken)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, keys)
|
||||
// 1 key is generated on startup, the other we manually generated.
|
||||
require.Equal(t, 2, len(keys.CryptoKeys))
|
||||
requireContainsKeys(t, keys.CryptoKeys, signingKey)
|
||||
requireNotContainsKeys(t, keys.CryptoKeys, encryptionKey)
|
||||
})
|
||||
|
||||
t.Run("InvalidFeature", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
cclient, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
proxy := coderdenttest.NewWorkspaceProxyReplica(t, api, cclient, &coderdenttest.ProxyOptions{
|
||||
Name: testutil.GetRandomName(t),
|
||||
})
|
||||
|
||||
_, err := proxy.SDKClient.CryptoKeys(ctx, codersdk.CryptoKeyFeatureOIDCConvert)
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
_, err = proxy.SDKClient.CryptoKeys(ctx, codersdk.CryptoKeyFeatureTailnetResume)
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
_, err = proxy.SDKClient.CryptoKeys(ctx, "invalid")
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
@@ -987,7 +1022,7 @@ func TestGetCryptoKeys(t *testing.T) {
|
||||
client := wsproxysdk.New(cclient.URL)
|
||||
client.SetSessionToken(cclient.SessionToken())
|
||||
|
||||
_, err := client.CryptoKeys(ctx)
|
||||
_, err := client.CryptoKeys(ctx, codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
@@ -995,6 +1030,18 @@ func TestGetCryptoKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func requireNotContainsKeys(t *testing.T, keys []codersdk.CryptoKey, unexpected ...codersdk.CryptoKey) {
|
||||
t.Helper()
|
||||
|
||||
for _, unexpectedKey := range unexpected {
|
||||
for _, key := range keys {
|
||||
if key.Feature == unexpectedKey.Feature && key.Sequence == unexpectedKey.Sequence {
|
||||
t.Fatalf("unexpected key %+v found", unexpectedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireContainsKeys(t *testing.T, keys []codersdk.CryptoKey, expected ...codersdk.CryptoKey) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -397,12 +397,12 @@ func TestCryptoKeys(t *testing.T) {
|
||||
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
||||
Secret: sql.NullString{String: "test", Valid: true},
|
||||
})
|
||||
key, err := crypt.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
|
||||
key, err := crypt.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", key.Secret.String)
|
||||
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
||||
|
||||
key, err = db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
|
||||
key, err = db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
require.NoError(t, err)
|
||||
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
|
||||
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
||||
@@ -415,7 +415,7 @@ func TestCryptoKeys(t *testing.T) {
|
||||
Secret: sql.NullString{String: "test", Valid: true},
|
||||
})
|
||||
key, err := crypt.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: key.Sequence,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -423,7 +423,7 @@ func TestCryptoKeys(t *testing.T) {
|
||||
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
||||
|
||||
key, err = db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: key.Sequence,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -459,7 +459,7 @@ func TestCryptoKeys(t *testing.T) {
|
||||
Secret: sql.NullString{String: "test", Valid: true},
|
||||
})
|
||||
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: 43,
|
||||
})
|
||||
keys, err := crypt.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeatureTailnetResume)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/apptest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -36,6 +37,9 @@ func TestWorkspaceApps(t *testing.T) {
|
||||
flushStatsCollectorCh <- flushStatsCollectorDone
|
||||
<-flushStatsCollectorDone
|
||||
}
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
|
||||
client, _, _, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: deploymentValues,
|
||||
@@ -51,6 +55,8 @@ func TestWorkspaceApps(t *testing.T) {
|
||||
},
|
||||
},
|
||||
WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user