blob: b000883ee6437fd5e51a28815fd7f615381ea7f8 [file] [log] [blame]
Lorenz Brun878f5f92020-05-12 16:15:39 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// The reconciler ensures that a base set of K8s resources is always available in the cluster. These are necessary to
18// ensure correct out-of-the-box functionality. All resources containing the smalltown.com/builtin=true label are assumed
19// to be managed by the reconciler.
20// It currently does not revert modifications made by admins, it is planned to create an admission plugin prohibiting
21// such modifications to resources with the smalltown.com/builtin label to deal with that problem. This would also solve a
22// potential issue where you could delete resources just by adding the smalltown.com/builtin=true label.
23package kubernetes
24
25import (
26 "context"
27 "time"
28
Lorenz Brunb15abad2020-04-16 11:17:12 +020029 storagev1 "k8s.io/api/storage/v1"
30
Lorenz Brun878f5f92020-05-12 16:15:39 +020031 "go.uber.org/zap"
32 corev1 "k8s.io/api/core/v1"
33 "k8s.io/api/policy/v1beta1"
34 rbacv1 "k8s.io/api/rbac/v1"
35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36 "k8s.io/client-go/kubernetes"
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +020037
38 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
Lorenz Brun878f5f92020-05-12 16:15:39 +020039)
40
41const builtinRBACPrefix = "smalltown:"
42
43// Sad workaround for all the pointer booleans in K8s specs
44func True() *bool {
45 val := true
46 return &val
47}
48func False() *bool {
49 val := false
50 return &val
51}
52
53func rbac(name string) string {
54 return builtinRBACPrefix + name
55}
56
57// Extended from https://github.com/kubernetes/kubernetes/blob/master/cluster/gce/addons/podsecuritypolicies/unprivileged-addon.yaml
58var builtinPSPs = []*v1beta1.PodSecurityPolicy{
59 {
60 ObjectMeta: metav1.ObjectMeta{
61 Name: "default",
62 Labels: map[string]string{
63 "smalltown.com/builtin": "true",
64 },
65 Annotations: map[string]string{
66 "kubernetes.io/description": "This default PSP allows the creation of pods using features that are" +
67 " generally considered safe against any sort of escape.",
68 },
69 },
70 Spec: v1beta1.PodSecurityPolicySpec{
71 AllowPrivilegeEscalation: True(),
72 AllowedCapabilities: []corev1.Capability{ // runc's default list of allowed capabilities
73 "SETPCAP",
74 "MKNOD",
75 "AUDIT_WRITE",
76 "CHOWN",
77 "NET_RAW",
78 "DAC_OVERRIDE",
79 "FOWNER",
80 "FSETID",
81 "KILL",
82 "SETGID",
83 "SETUID",
84 "NET_BIND_SERVICE",
85 "SYS_CHROOT",
86 "SETFCAP",
87 },
88 HostNetwork: false,
89 HostIPC: false,
90 HostPID: false,
91 FSGroup: v1beta1.FSGroupStrategyOptions{
92 Rule: v1beta1.FSGroupStrategyRunAsAny,
93 },
94 RunAsUser: v1beta1.RunAsUserStrategyOptions{
95 Rule: v1beta1.RunAsUserStrategyRunAsAny,
96 },
97 SELinux: v1beta1.SELinuxStrategyOptions{
98 Rule: v1beta1.SELinuxStrategyRunAsAny,
99 },
100 SupplementalGroups: v1beta1.SupplementalGroupsStrategyOptions{
101 Rule: v1beta1.SupplementalGroupsStrategyRunAsAny,
102 },
103 Volumes: []v1beta1.FSType{ // Volumes considered safe to use
104 v1beta1.ConfigMap,
105 v1beta1.EmptyDir,
106 v1beta1.Projected,
107 v1beta1.Secret,
108 v1beta1.DownwardAPI,
109 v1beta1.PersistentVolumeClaim,
110 },
111 },
112 },
113}
114
115var builtinClusterRoles = []*rbacv1.ClusterRole{
116 {
117 ObjectMeta: metav1.ObjectMeta{
118 Name: rbac("psp-default"),
Lorenz Brun4cc664d2020-06-02 16:08:24 +0200119 Labels: map[string]string{
120 "smalltown.com/builtin": "true",
121 },
Lorenz Brun878f5f92020-05-12 16:15:39 +0200122 Annotations: map[string]string{
123 "kubernetes.io/description": "This role grants access to the \"default\" PSP.",
124 },
125 },
126 Rules: []rbacv1.PolicyRule{
127 {
128 APIGroups: []string{"policy"},
129 Resources: []string{"podsecuritypolicies"},
130 ResourceNames: []string{"default"},
131 Verbs: []string{"use"},
132 },
133 },
134 },
135}
136
137var builtinClusterRoleBindings = []*rbacv1.ClusterRoleBinding{
138 {
139 ObjectMeta: metav1.ObjectMeta{
140 Name: rbac("default-psp-for-sa"),
Lorenz Brun4cc664d2020-06-02 16:08:24 +0200141 Labels: map[string]string{
142 "smalltown.com/builtin": "true",
143 },
Lorenz Brun878f5f92020-05-12 16:15:39 +0200144 Annotations: map[string]string{
145 "kubernetes.io/description": "This binding grants every service account access to the \"default\" PSP. " +
146 "Creation of Pods is still restricted by other RBAC roles. Otherwise no pods (unprivileged or not) " +
147 "can be created.",
148 },
149 },
150 RoleRef: rbacv1.RoleRef{
151 APIGroup: rbacv1.GroupName,
152 Kind: "ClusterRole",
153 Name: rbac("psp-default"),
154 },
155 Subjects: []rbacv1.Subject{
156 {
157 APIGroup: rbacv1.GroupName,
158 Kind: "Group",
159 Name: "system:serviceaccounts",
160 },
161 },
162 },
163 {
164 ObjectMeta: metav1.ObjectMeta{
165 Name: rbac("apiserver-kubelet-client"),
Lorenz Brun4cc664d2020-06-02 16:08:24 +0200166 Labels: map[string]string{
167 "smalltown.com/builtin": "true",
168 },
Lorenz Brun878f5f92020-05-12 16:15:39 +0200169 Annotations: map[string]string{
170 "kubernetes.io/description": "This binding grants the apiserver access to the kubelets. This enables " +
171 "lots of built-in functionality like reading logs or forwarding ports via the API.",
172 },
173 },
174 RoleRef: rbacv1.RoleRef{
175 APIGroup: rbacv1.GroupName,
176 Kind: "ClusterRole",
177 Name: "system:kubelet-api-admin",
178 },
179 Subjects: []rbacv1.Subject{
180 {
181 APIGroup: rbacv1.GroupName,
182 Kind: "User",
183 Name: "smalltown:apiserver-kubelet-client",
184 },
185 },
186 },
187}
188
Lorenz Brunb15abad2020-04-16 11:17:12 +0200189var reclaimPolicyDelete = corev1.PersistentVolumeReclaimDelete
190var waitForConsumerBinding = storagev1.VolumeBindingWaitForFirstConsumer
Lorenz Brun878f5f92020-05-12 16:15:39 +0200191
Lorenz Brunb15abad2020-04-16 11:17:12 +0200192var builtinStorageClasses = []*storagev1.StorageClass{
193 {
194 ObjectMeta: metav1.ObjectMeta{
195 Name: "local",
196 Annotations: map[string]string{
197 "storageclass.kubernetes.io/is-default-class": "true",
198 },
199 Labels: map[string]string{
200 "smalltown.com/builtin": "true",
201 },
202 },
203 AllowVolumeExpansion: True(),
204 Provisioner: csiProvisionerName,
205 ReclaimPolicy: &reclaimPolicyDelete,
206 VolumeBindingMode: &waitForConsumerBinding,
207 },
208}
209
210var builtinCSIDrivers = []*storagev1.CSIDriver{
211 {
212 ObjectMeta: metav1.ObjectMeta{
213 Name: csiProvisionerName,
214 Labels: map[string]string{
215 "smalltown.com/builtin": "true",
216 },
217 },
218 Spec: storagev1.CSIDriverSpec{
219 AttachRequired: False(),
220 PodInfoOnMount: False(),
221 VolumeLifecycleModes: []storagev1.VolumeLifecycleMode{storagev1.VolumeLifecyclePersistent},
222 },
223 },
224}
225
226type reconciler func(context.Context, kubernetes.Interface) error
227
228func runReconciler(clientSet kubernetes.Interface) supervisor.Runnable {
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200229 return func(ctx context.Context) error {
230 log := supervisor.Logger(ctx)
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200231 reconcilers := map[string]reconciler{
232 "psps": reconcilePSPs,
233 "clusterroles": reconcileClusterRoles,
234 "clusterrolebindings": reconcileClusterRoleBindings,
Lorenz Brunb15abad2020-04-16 11:17:12 +0200235 "storageclasses": reconcileSCs,
236 "csidrivers": reconcileCSIDrivers,
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200237 }
238 t := time.NewTicker(10 * time.Second)
239 reconcile := func() {
240 for name, reconciler := range reconcilers {
241 if err := reconciler(ctx, clientSet); err != nil {
242 log.Warn("Failed to reconcile built-in resources", zap.String("kind", name), zap.Error(err))
243 }
Lorenz Brun878f5f92020-05-12 16:15:39 +0200244 }
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200245 }
246 supervisor.Signal(ctx, supervisor.SignalHealthy)
247 reconcile()
248 for {
249 select {
250 case <-t.C:
251 reconcile()
252 case <-ctx.Done():
253 return nil
254 }
Lorenz Brun878f5f92020-05-12 16:15:39 +0200255 }
256 }
257}
258
Lorenz Brunb15abad2020-04-16 11:17:12 +0200259func reconcilePSPs(ctx context.Context, clientSet kubernetes.Interface) error {
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200260 pspClient := clientSet.PolicyV1beta1().PodSecurityPolicies()
Lorenz Brun878f5f92020-05-12 16:15:39 +0200261 availablePSPs, err := pspClient.List(ctx, metav1.ListOptions{
262 LabelSelector: "smalltown.com/builtin=true",
263 })
264 if err != nil {
265 return err
266 }
267 availablePSPMap := make(map[string]struct{})
268 for _, psp := range availablePSPs.Items {
269 availablePSPMap[psp.Name] = struct{}{}
270 }
271 expectedPSPMap := make(map[string]*v1beta1.PodSecurityPolicy)
272 for _, psp := range builtinPSPs {
273 expectedPSPMap[psp.Name] = psp
274 }
275 for pspName, psp := range expectedPSPMap {
276 if _, ok := availablePSPMap[pspName]; !ok {
277 if _, err := pspClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
278 return err
279 }
280 }
281 }
282 for pspName, _ := range availablePSPMap {
283 if _, ok := expectedPSPMap[pspName]; !ok {
284 if err := pspClient.Delete(ctx, pspName, metav1.DeleteOptions{}); err != nil {
285 return err
286 }
287 }
288 }
289 return nil
290}
291
Lorenz Brunb15abad2020-04-16 11:17:12 +0200292func reconcileClusterRoles(ctx context.Context, clientSet kubernetes.Interface) error {
Lorenz Brun8e3b8fc2020-05-19 14:29:40 +0200293 crClient := clientSet.RbacV1().ClusterRoles()
Lorenz Brun878f5f92020-05-12 16:15:39 +0200294 availableCRs, err := crClient.List(ctx, metav1.ListOptions{
295 LabelSelector: "smalltown.com/builtin=true",
296 })
297 if err != nil {
298 return err
299 }
300 availableCRMap := make(map[string]struct{})
301 for _, cr := range availableCRs.Items {
302 availableCRMap[cr.Name] = struct{}{}
303 }
304 expectedCRMap := make(map[string]*rbacv1.ClusterRole)
305 for _, cr := range builtinClusterRoles {
306 expectedCRMap[cr.Name] = cr
307 }
308 for crName, psp := range expectedCRMap {
309 if _, ok := availableCRMap[crName]; !ok {
310 if _, err := crClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
311 return err
312 }
313 }
314 }
315 for crName, _ := range availableCRMap {
316 if _, ok := expectedCRMap[crName]; !ok {
317 if err := crClient.Delete(ctx, crName, metav1.DeleteOptions{}); err != nil {
318 return err
319 }
320 }
321 }
322 return nil
323}
324
Lorenz Brunb15abad2020-04-16 11:17:12 +0200325func reconcileClusterRoleBindings(ctx context.Context, clientset kubernetes.Interface) error {
Lorenz Brun878f5f92020-05-12 16:15:39 +0200326 crbClient := clientset.RbacV1().ClusterRoleBindings()
327 availableCRBs, err := crbClient.List(ctx, metav1.ListOptions{
328 LabelSelector: "smalltown.com/builtin=true",
329 })
330 if err != nil {
331 return err
332 }
333 availableCRBMap := make(map[string]struct{})
334 for _, crb := range availableCRBs.Items {
335 availableCRBMap[crb.Name] = struct{}{}
336 }
337 expectedCRBMap := make(map[string]*rbacv1.ClusterRoleBinding)
338 for _, crb := range builtinClusterRoleBindings {
339 expectedCRBMap[crb.Name] = crb
340 }
341 for crbName, psp := range expectedCRBMap {
342 if _, ok := availableCRBMap[crbName]; !ok {
343 if _, err := crbClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
344 return err
345 }
346 }
347 }
348 for crbName, _ := range availableCRBMap {
349 if _, ok := expectedCRBMap[crbName]; !ok {
350 if err := crbClient.Delete(ctx, crbName, metav1.DeleteOptions{}); err != nil {
351 return err
352 }
353 }
354 }
355 return nil
356}
Lorenz Brunb15abad2020-04-16 11:17:12 +0200357
358func reconcileSCs(ctx context.Context, clientSet kubernetes.Interface) error {
359 scsClient := clientSet.StorageV1().StorageClasses()
360 availableSCs, err := scsClient.List(ctx, metav1.ListOptions{
361 LabelSelector: "smalltown.com/builtin=true",
362 })
363 if err != nil {
364 return err
365 }
366 availableSCMap := make(map[string]struct{})
367 for _, sc := range availableSCs.Items {
368 availableSCMap[sc.Name] = struct{}{}
369 }
370 expectedSCMap := make(map[string]*storagev1.StorageClass)
371 for _, sc := range builtinStorageClasses {
372 expectedSCMap[sc.Name] = sc
373 }
374 for scName, sc := range expectedSCMap {
375 if _, ok := availableSCMap[scName]; !ok {
376 if _, err := scsClient.Create(ctx, sc, metav1.CreateOptions{}); err != nil {
377 return err
378 }
379 }
380 }
381 for pspName, _ := range availableSCMap {
382 if _, ok := expectedSCMap[pspName]; !ok {
383 if err := scsClient.Delete(ctx, pspName, metav1.DeleteOptions{}); err != nil {
384 return err
385 }
386 }
387 }
388 return nil
389}
390
391func reconcileCSIDrivers(ctx context.Context, clientSet kubernetes.Interface) error {
392 drvClient := clientSet.StorageV1().CSIDrivers()
393 availableDrvs, err := drvClient.List(ctx, metav1.ListOptions{
394 LabelSelector: "smalltown.com/builtin=true",
395 })
396 if err != nil {
397 return err
398 }
399 availableDrvMap := make(map[string]struct{})
400 for _, drv := range availableDrvs.Items {
401 availableDrvMap[drv.Name] = struct{}{}
402 }
403 expectedDrvMap := make(map[string]*storagev1.CSIDriver)
404 for _, drv := range builtinCSIDrivers {
405 expectedDrvMap[drv.Name] = drv
406 }
407 for drvName, drv := range expectedDrvMap {
408 if _, ok := availableDrvMap[drvName]; !ok {
409 if _, err := drvClient.Create(ctx, drv, metav1.CreateOptions{}); err != nil {
410 return err
411 }
412 }
413 }
414 for pspName, _ := range availableDrvMap {
415 if _, ok := expectedDrvMap[pspName]; !ok {
416 if err := drvClient.Delete(ctx, pspName, metav1.DeleteOptions{}); err != nil {
417 return err
418 }
419 }
420 }
421 return nil
422}