Skip to content

Commit c800175

Browse files
authored
[ws-manager-mk2] Protect tokens (#16806)
* [wsman-mk2] Create token secret * [installer] Fix casing for namespace type metadata * [installer] Create secrets namespace * [installer] Configure roles and bindings * [installer] Move namespace constant to common pkg * [installer] Create permissions for ws-daemon * [wsman-mk2] Watch on multiple ns * [ws-daemon] Use token secret * [wsman-mk2] Remove secret from initializer * [wsman-mk2] Test token secret * [werft] Fix document index * [installer] Update render tests * [wsman-mk2] Fix imports * [wsman-mk2] Ensure maintenance controller required permissions * [wsman-mk2] Retry deleting secret * [installer] Ensure objects in secrets namespace are only created with mk2 option
1 parent d5c9519 commit c800175

File tree

41 files changed

+496
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+496
-224
lines changed

.werft/jobs/build/installer/post-process.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ MATCHES="$(grep -c -- --- k8s.yaml)"
5252
# get the read number of K8s manifest docs
5353
# K8s object names and kinds are duplicated in a config map to faciliate deletion
5454
# subtract one (the config map) and then divide by 2 to get the actual # of docs we'll loop through
55-
DOCS="$((((MATCHES - 1) / 2) + 1))"
55+
DOCS="$(((MATCHES - 1) / 2))"
5656
documentIndex=0
5757

5858
while [ "$documentIndex" -le "$DOCS" ]; do

components/ws-daemon/pkg/controller/workspace_controller.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222
"github.com/prometheus/client_golang/prometheus"
2323

2424
"google.golang.org/protobuf/proto"
25+
corev1 "k8s.io/api/core/v1"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/types"
2628
"k8s.io/apimachinery/pkg/util/wait"
2729
"k8s.io/client-go/util/retry"
2830
ctrl "sigs.k8s.io/controller-runtime"
@@ -55,9 +57,10 @@ type WorkspaceController struct {
5557
maxConcurrentReconciles int
5658
operations *WorkspaceOperations
5759
metrics *workspaceMetrics
60+
secretNamespace string
5861
}
5962

60-
func NewWorkspaceController(c client.Client, nodeName string, maxConcurrentReconciles int, ops *WorkspaceOperations, reg prometheus.Registerer) (*WorkspaceController, error) {
63+
func NewWorkspaceController(c client.Client, nodeName, secretNamespace string, maxConcurrentReconciles int, ops *WorkspaceOperations, reg prometheus.Registerer) (*WorkspaceController, error) {
6164
metrics := newWorkspaceMetrics()
6265
reg.Register(metrics)
6366

@@ -67,6 +70,7 @@ func NewWorkspaceController(c client.Client, nodeName string, maxConcurrentRecon
6770
maxConcurrentReconciles: maxConcurrentReconciles,
6871
operations: ops,
6972
metrics: metrics,
73+
secretNamespace: secretNamespace,
7074
}, nil
7175
}
7276

@@ -139,10 +143,8 @@ func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *wor
139143
defer tracing.FinishSpan(span, &err)
140144

141145
if c := wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionContentReady)); c == nil {
142-
var init csapi.WorkspaceInitializer
143-
err = proto.Unmarshal(ws.Spec.Initializer, &init)
146+
init, err := wsc.prepareInitializer(ctx, ws)
144147
if err != nil {
145-
err = fmt.Errorf("cannot unmarshal initializer config: %w", err)
146148
return ctrl.Result{}, err
147149
}
148150

@@ -153,7 +155,7 @@ func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *wor
153155
WorkspaceId: ws.Spec.Ownership.WorkspaceID,
154156
InstanceId: ws.Name,
155157
},
156-
Initializer: &init,
158+
Initializer: init,
157159
Headless: ws.IsHeadless(),
158160
})
159161

@@ -300,6 +302,27 @@ func (wsc *WorkspaceController) handleWorkspaceStop(ctx context.Context, ws *wor
300302
return ctrl.Result{}, err
301303
}
302304

305+
func (wsc *WorkspaceController) prepareInitializer(ctx context.Context, ws *workspacev1.Workspace) (*csapi.WorkspaceInitializer, error) {
306+
var init csapi.WorkspaceInitializer
307+
err := proto.Unmarshal(ws.Spec.Initializer, &init)
308+
if err != nil {
309+
err = fmt.Errorf("cannot unmarshal initializer config: %w", err)
310+
return nil, err
311+
}
312+
313+
var tokenSecret corev1.Secret
314+
err = wsc.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s-tokens", ws.Name), Namespace: wsc.secretNamespace}, &tokenSecret)
315+
if err != nil {
316+
return nil, fmt.Errorf("could not get token secret for workspace: %w", err)
317+
}
318+
319+
if err = csapi.InjectSecretsToInitializer(&init, tokenSecret.Data); err != nil {
320+
return nil, fmt.Errorf("failed to inject secrets into initializer: %w", err)
321+
}
322+
323+
return &init, nil
324+
}
325+
303326
func toWorkspaceGitStatus(status *csapi.GitStatus) *workspacev1.GitStatus {
304327
if status == nil {
305328
return nil

components/ws-daemon/pkg/daemon/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type RuntimeConfig struct {
4242
Container *container.Config `json:"containerRuntime"`
4343
Kubeconfig string `json:"kubeconfig"`
4444
KubernetesNamespace string `json:"namespace"`
45+
SecretsNamespace string `json:"secretsNamespace"`
4546
}
4647

4748
type IOLimitConfig struct {

components/ws-daemon/pkg/daemon/daemon.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"k8s.io/client-go/rest"
2323
"k8s.io/client-go/tools/clientcmd"
2424
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/cache"
2526
"sigs.k8s.io/controller-runtime/pkg/manager"
2627
"sigs.k8s.io/controller-runtime/pkg/metrics"
2728

@@ -175,6 +176,7 @@ func NewDaemon(config Config) (*Daemon, error) {
175176
Namespace: config.Runtime.KubernetesNamespace,
176177
HealthProbeBindAddress: "0",
177178
MetricsBindAddress: "0", // Metrics are exposed through baseserver.
179+
NewCache: cache.MultiNamespacedCacheBuilder([]string{config.Runtime.KubernetesNamespace, config.Runtime.SecretsNamespace}),
178180
})
179181
if err != nil {
180182
return nil, err
@@ -207,7 +209,8 @@ func NewDaemon(config Config) (*Daemon, error) {
207209
return nil, err
208210
}
209211

210-
wsctrl, err := controller.NewWorkspaceController(mgr.GetClient(), nodename, config.WorkspaceController.MaxConcurrentReconciles, workspaceOps, wrappedReg)
212+
wsctrl, err := controller.NewWorkspaceController(
213+
mgr.GetClient(), nodename, config.Runtime.SecretsNamespace, config.WorkspaceController.MaxConcurrentReconciles, workspaceOps, wrappedReg)
211214
if err != nil {
212215
return nil, err
213216
}

components/ws-manager-api/go/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ type ServiceConfiguration struct {
7777
type Configuration struct {
7878
// Namespace is the kubernetes namespace the workspace manager operates in
7979
Namespace string `json:"namespace"`
80+
// SecretsNamespace is the kubernetes namespace which contains workspace secrets
81+
SecretsNamespace string `json:"secretsNamespace"`
8082
// SchedulerName is the name of the workspace scheduler all pods are created with
8183
SchedulerName string `json:"schedulerName"`
8284
// SeccompProfile names the seccomp profile workspaces will use

components/ws-manager-mk2/controllers/maintenance_controller.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ func (r *MaintenanceReconciler) Reconcile(ctx context.Context, req ctrl.Request)
5050
log := log.FromContext(ctx).WithValues("configMap", req.NamespacedName)
5151

5252
if req.Name != configMapName {
53-
log.Info("ignoring unexpected ConfigMap")
5453
return ctrl.Result{}, nil
5554
}
5655

components/ws-manager-mk2/controllers/suite_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ import (
2424
"github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/activity"
2525
"github.com/gitpod-io/gitpod/ws-manager/api/config"
2626
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
27+
corev1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2729
//+kubebuilder:scaffold:imports
2830
)
2931

3032
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
3133
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
3234

3335
const (
34-
timeout = time.Second * 20
35-
duration = time.Second * 2
36-
interval = time.Millisecond * 250
36+
timeout = time.Second * 20
37+
duration = time.Second * 2
38+
interval = time.Millisecond * 250
39+
secretsNamespace = "workspace-secrets"
3740
)
3841

3942
// var cfg *rest.Config
@@ -113,6 +116,7 @@ var _ = BeforeSuite(func() {
113116
Expect(timeoutReconciler.SetupWithManager(k8sManager)).To(Succeed())
114117

115118
ctx, cancel = context.WithCancel(context.Background())
119+
_ = createNamespace(secretsNamespace)
116120

117121
go func() {
118122
defer GinkgoRecover()
@@ -127,6 +131,7 @@ func newTestConfig() config.Configuration {
127131
GitpodHostURL: "gitpod.io",
128132
HeartbeatInterval: util.Duration(30 * time.Second),
129133
Namespace: "default",
134+
SecretsNamespace: secretsNamespace,
130135
SeccompProfile: "default.json",
131136
Timeouts: config.WorkspaceTimeoutConfiguration{
132137
AfterClose: util.Duration(1 * time.Minute),
@@ -156,6 +161,19 @@ func (f *fakeMaintenance) IsEnabled() bool {
156161
return f.enabled
157162
}
158163

164+
func createNamespace(name string) *corev1.Namespace {
165+
GinkgoHelper()
166+
167+
namespace := &corev1.Namespace{
168+
ObjectMeta: metav1.ObjectMeta{
169+
Name: name,
170+
},
171+
}
172+
173+
Expect(k8sClient.Create(ctx, namespace)).To(Succeed())
174+
return namespace
175+
}
176+
159177
var _ = AfterSuite(func() {
160178
cancel()
161179
By("tearing down the test environment")

components/ws-manager-mk2/controllers/workspace_controller.go

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ package controllers
77
import (
88
"context"
99
"fmt"
10+
"strings"
1011
"time"
1112

1213
corev1 "k8s.io/api/core/v1"
1314
"k8s.io/apimachinery/pkg/api/errors"
1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
"k8s.io/apimachinery/pkg/runtime"
1617
"k8s.io/apimachinery/pkg/types"
18+
"k8s.io/apimachinery/pkg/util/wait"
1719
ctrl "sigs.k8s.io/controller-runtime"
1820
"sigs.k8s.io/controller-runtime/pkg/client"
1921
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -195,7 +197,9 @@ func (r *WorkspaceReconciler) actOnStatus(ctx context.Context, workspace *worksp
195197
}
196198
}
197199

198-
r.deleteWorkspaceSecrets(ctx, workspace)
200+
if err := r.deleteWorkspaceSecrets(ctx, workspace); err != nil {
201+
return ctrl.Result{RequeueAfter: 10 * time.Second}, err
202+
}
199203

200204
// Workspace might have already been in a deleting state,
201205
// but not guaranteed, so try deleting anyway.
@@ -257,7 +261,10 @@ func (r *WorkspaceReconciler) actOnStatus(ctx context.Context, workspace *worksp
257261
}
258262

259263
case workspace.Status.Phase == workspacev1.WorkspacePhaseRunning:
260-
r.deleteWorkspaceSecrets(ctx, workspace)
264+
err := r.deleteWorkspaceSecrets(ctx, workspace)
265+
if err != nil {
266+
log.Error(err, "could not delete workspace secrets")
267+
}
261268

262269
// we've disposed already - try to remove the finalizer and call it a day
263270
case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped:
@@ -349,37 +356,64 @@ func (r *WorkspaceReconciler) deleteWorkspacePod(ctx context.Context, pod *corev
349356
return ctrl.Result{}, nil
350357
}
351358

352-
func (r *WorkspaceReconciler) deleteWorkspaceSecrets(ctx context.Context, ws *workspacev1.Workspace) {
359+
func (r *WorkspaceReconciler) deleteWorkspaceSecrets(ctx context.Context, ws *workspacev1.Workspace) error {
353360
log := log.FromContext(ctx)
354361

355362
// if a secret cannot be deleted we do not return early because we want to attempt
356363
// the deletion of the remaining secrets
364+
var errs []string
357365
err := r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "env"), r.Config.Namespace)
358366
if err != nil {
367+
errs = append(errs, err.Error())
359368
log.Error(err, "could not delete environment secret", "workspace", ws.Name)
360369
}
361-
}
362-
363-
func (r *WorkspaceReconciler) deleteSecret(ctx context.Context, name, namespace string) error {
364-
var secret corev1.Secret
365-
err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &secret)
366-
if errors.IsNotFound(err) {
367-
// nothing to delete
368-
return nil
369-
}
370370

371+
err = r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "tokens"), r.Config.SecretsNamespace)
371372
if err != nil {
372-
return fmt.Errorf("could not retrieve secret %s: %w", name, err)
373+
errs = append(errs, err.Error())
374+
log.Error(err, "could not delete token secret", "workspace", ws.Name)
373375
}
374376

375-
err = r.Client.Delete(ctx, &secret)
376-
if err != nil && !errors.IsNotFound(err) {
377-
return fmt.Errorf("could not delete secret %s: %w", name, err)
377+
if len(errs) != 0 {
378+
return fmt.Errorf(strings.Join(errs, ":"))
378379
}
379380

380381
return nil
381382
}
382383

384+
func (r *WorkspaceReconciler) deleteSecret(ctx context.Context, name, namespace string) error {
385+
log := log.FromContext(ctx)
386+
387+
err := wait.ExponentialBackoffWithContext(ctx, wait.Backoff{
388+
Duration: 100 * time.Millisecond,
389+
Factor: 1.5,
390+
Jitter: 0.2,
391+
Steps: 3,
392+
}, func() (bool, error) {
393+
var secret corev1.Secret
394+
err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &secret)
395+
if errors.IsNotFound(err) {
396+
// nothing to delete
397+
return true, nil
398+
}
399+
400+
if err != nil {
401+
log.Error(err, "cannot retrieve secret scheduled for deletion", "secret", name)
402+
return false, nil
403+
}
404+
405+
err = r.Client.Delete(ctx, &secret)
406+
if err != nil && !errors.IsNotFound(err) {
407+
log.Error(err, "cannot delete secret", "secret", name)
408+
return false, nil
409+
}
410+
411+
return true, nil
412+
})
413+
414+
return err
415+
}
416+
383417
var (
384418
wsOwnerKey = ".metadata.controller"
385419
apiGVStr = workspacev1.GroupVersion.String()

components/ws-manager-mk2/controllers/workspace_controller_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ var _ = Describe("WorkspaceController", func() {
3333
Context("with regular workspaces", func() {
3434
It("should handle successful workspace creation and stop request", func() {
3535
name := uuid.NewString()
36+
37+
envSecret := createSecret(fmt.Sprintf("%s-env", name), "default")
38+
tokenSecret := createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
39+
3640
ws := newWorkspace(name, "default")
37-
secret := createSecret(fmt.Sprintf("%s-env", name), "default")
3841
m := collectMetricCounts(wsMetrics, ws)
3942
pod := createWorkspaceExpectPod(ws)
4043

@@ -73,7 +76,8 @@ var _ = Describe("WorkspaceController", func() {
7376
})
7477

7578
expectPhaseEventually(ws, workspacev1.WorkspacePhaseRunning)
76-
expectSecretCleanup(secret)
79+
expectSecretCleanup(envSecret)
80+
expectSecretCleanup(tokenSecret)
7781

7882
markContentReady(ws)
7983

@@ -255,7 +259,10 @@ var _ = Describe("WorkspaceController", func() {
255259
It("deleting workspace resource should gracefully clean up", func() {
256260
name := uuid.NewString()
257261
ws := newWorkspace(name, "default")
258-
secret := createSecret(fmt.Sprintf("%s-env", name), "default")
262+
263+
envSecret := createSecret(fmt.Sprintf("%s-env", name), "default")
264+
tokenSecret := createSecret(fmt.Sprintf("%s-tokens", name), secretsNamespace)
265+
259266
m := collectMetricCounts(wsMetrics, ws)
260267
pod := createWorkspaceExpectPod(ws)
261268

@@ -269,7 +276,8 @@ var _ = Describe("WorkspaceController", func() {
269276

270277
expectWorkspaceCleanup(ws, pod)
271278

272-
expectSecretCleanup(secret)
279+
expectSecretCleanup(envSecret)
280+
expectSecretCleanup(tokenSecret)
273281

274282
expectMetricsDelta(m, collectMetricCounts(wsMetrics, ws), metricCounts{
275283
restores: 1,

components/ws-manager-mk2/main.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ import (
1818
"google.golang.org/grpc/credentials"
1919
"google.golang.org/grpc/credentials/insecure"
2020
_ "k8s.io/client-go/plugin/pkg/client/auth"
21-
"k8s.io/client-go/rest"
2221

2322
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
2423
"github.com/prometheus/client_golang/prometheus"
25-
corev1 "k8s.io/api/core/v1"
26-
"k8s.io/apimachinery/pkg/labels"
2724
"k8s.io/apimachinery/pkg/runtime"
2825
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2926
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -109,16 +106,7 @@ func main() {
109106
HealthProbeBindAddress: cfg.Health.Addr,
110107
LeaderElection: enableLeaderElection,
111108
LeaderElectionID: "ws-manager-mk2-leader.gitpod.io",
112-
Namespace: cfg.Manager.Namespace,
113-
NewCache: func(conf *rest.Config, opts cache.Options) (cache.Cache, error) {
114-
// Only watch the maintenance mode ConfigMap.
115-
opts.SelectorsByObject = cache.SelectorsByObject{
116-
&corev1.ConfigMap{}: cache.ObjectSelector{
117-
Label: labels.SelectorFromSet(labels.Set{controllers.LabelMaintenance: "true"}),
118-
},
119-
}
120-
return cache.New(conf, opts)
121-
},
109+
NewCache: cache.MultiNamespacedCacheBuilder([]string{cfg.Manager.Namespace, cfg.Manager.SecretsNamespace}),
122110
})
123111
if err != nil {
124112
setupLog.Error(err, "unable to start manager")

0 commit comments

Comments
 (0)