blob: cf991ce5b00409302c0c87b6a8f51384a0fab7d8 [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
29 "go.uber.org/zap"
30 corev1 "k8s.io/api/core/v1"
31 "k8s.io/api/policy/v1beta1"
32 rbacv1 "k8s.io/api/rbac/v1"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/client-go/kubernetes"
35 "k8s.io/client-go/tools/clientcmd"
36)
37
38const builtinRBACPrefix = "smalltown:"
39
40// Sad workaround for all the pointer booleans in K8s specs
41func True() *bool {
42 val := true
43 return &val
44}
45func False() *bool {
46 val := false
47 return &val
48}
49
50func rbac(name string) string {
51 return builtinRBACPrefix + name
52}
53
54// Extended from https://github.com/kubernetes/kubernetes/blob/master/cluster/gce/addons/podsecuritypolicies/unprivileged-addon.yaml
55var builtinPSPs = []*v1beta1.PodSecurityPolicy{
56 {
57 ObjectMeta: metav1.ObjectMeta{
58 Name: "default",
59 Labels: map[string]string{
60 "smalltown.com/builtin": "true",
61 },
62 Annotations: map[string]string{
63 "kubernetes.io/description": "This default PSP allows the creation of pods using features that are" +
64 " generally considered safe against any sort of escape.",
65 },
66 },
67 Spec: v1beta1.PodSecurityPolicySpec{
68 AllowPrivilegeEscalation: True(),
69 AllowedCapabilities: []corev1.Capability{ // runc's default list of allowed capabilities
70 "SETPCAP",
71 "MKNOD",
72 "AUDIT_WRITE",
73 "CHOWN",
74 "NET_RAW",
75 "DAC_OVERRIDE",
76 "FOWNER",
77 "FSETID",
78 "KILL",
79 "SETGID",
80 "SETUID",
81 "NET_BIND_SERVICE",
82 "SYS_CHROOT",
83 "SETFCAP",
84 },
85 HostNetwork: false,
86 HostIPC: false,
87 HostPID: false,
88 FSGroup: v1beta1.FSGroupStrategyOptions{
89 Rule: v1beta1.FSGroupStrategyRunAsAny,
90 },
91 RunAsUser: v1beta1.RunAsUserStrategyOptions{
92 Rule: v1beta1.RunAsUserStrategyRunAsAny,
93 },
94 SELinux: v1beta1.SELinuxStrategyOptions{
95 Rule: v1beta1.SELinuxStrategyRunAsAny,
96 },
97 SupplementalGroups: v1beta1.SupplementalGroupsStrategyOptions{
98 Rule: v1beta1.SupplementalGroupsStrategyRunAsAny,
99 },
100 Volumes: []v1beta1.FSType{ // Volumes considered safe to use
101 v1beta1.ConfigMap,
102 v1beta1.EmptyDir,
103 v1beta1.Projected,
104 v1beta1.Secret,
105 v1beta1.DownwardAPI,
106 v1beta1.PersistentVolumeClaim,
107 },
108 },
109 },
110}
111
112var builtinClusterRoles = []*rbacv1.ClusterRole{
113 {
114 ObjectMeta: metav1.ObjectMeta{
115 Name: rbac("psp-default"),
116 Annotations: map[string]string{
117 "kubernetes.io/description": "This role grants access to the \"default\" PSP.",
118 },
119 },
120 Rules: []rbacv1.PolicyRule{
121 {
122 APIGroups: []string{"policy"},
123 Resources: []string{"podsecuritypolicies"},
124 ResourceNames: []string{"default"},
125 Verbs: []string{"use"},
126 },
127 },
128 },
129}
130
131var builtinClusterRoleBindings = []*rbacv1.ClusterRoleBinding{
132 {
133 ObjectMeta: metav1.ObjectMeta{
134 Name: rbac("default-psp-for-sa"),
135 Annotations: map[string]string{
136 "kubernetes.io/description": "This binding grants every service account access to the \"default\" PSP. " +
137 "Creation of Pods is still restricted by other RBAC roles. Otherwise no pods (unprivileged or not) " +
138 "can be created.",
139 },
140 },
141 RoleRef: rbacv1.RoleRef{
142 APIGroup: rbacv1.GroupName,
143 Kind: "ClusterRole",
144 Name: rbac("psp-default"),
145 },
146 Subjects: []rbacv1.Subject{
147 {
148 APIGroup: rbacv1.GroupName,
149 Kind: "Group",
150 Name: "system:serviceaccounts",
151 },
152 },
153 },
154 {
155 ObjectMeta: metav1.ObjectMeta{
156 Name: rbac("apiserver-kubelet-client"),
157 Annotations: map[string]string{
158 "kubernetes.io/description": "This binding grants the apiserver access to the kubelets. This enables " +
159 "lots of built-in functionality like reading logs or forwarding ports via the API.",
160 },
161 },
162 RoleRef: rbacv1.RoleRef{
163 APIGroup: rbacv1.GroupName,
164 Kind: "ClusterRole",
165 Name: "system:kubelet-api-admin",
166 },
167 Subjects: []rbacv1.Subject{
168 {
169 APIGroup: rbacv1.GroupName,
170 Kind: "User",
171 Name: "smalltown:apiserver-kubelet-client",
172 },
173 },
174 },
175}
176
177func runReconciler(ctx context.Context, masterKubeconfig []byte, log *zap.Logger) error {
178 rawClientConfig, err := clientcmd.NewClientConfigFromBytes(masterKubeconfig)
179 if err != nil {
180 return err
181 }
182
183 clientConfig, err := rawClientConfig.ClientConfig()
184 clientset, err := kubernetes.NewForConfig(clientConfig)
185 if err != nil {
186 return err
187 }
188 t := time.NewTicker(10 * time.Second)
189 for {
190 err = reconcile(ctx, clientset)
191 select {
192 case <-t.C:
193 err = reconcile(ctx, clientset)
194 if err != nil {
195 log.Warn("Failed to reconcile built-in resources", zap.Error(err))
196 }
197 case <-ctx.Done():
198 return nil
199 }
200 }
201}
202
203func reconcile(ctx context.Context, clientset *kubernetes.Clientset) error {
204 if err := reconcilePSPs(ctx, clientset); err != nil {
205 return err
206 }
207 if err := reconcileClusterRoles(ctx, clientset); err != nil {
208 return err
209 }
210 if err := reconcileClusterRoleBindings(ctx, clientset); err != nil {
211 return err
212 }
213 return nil
214}
215
216func reconcilePSPs(ctx context.Context, clientset *kubernetes.Clientset) error {
217 pspClient := clientset.PolicyV1beta1().PodSecurityPolicies()
218 availablePSPs, err := pspClient.List(ctx, metav1.ListOptions{
219 LabelSelector: "smalltown.com/builtin=true",
220 })
221 if err != nil {
222 return err
223 }
224 availablePSPMap := make(map[string]struct{})
225 for _, psp := range availablePSPs.Items {
226 availablePSPMap[psp.Name] = struct{}{}
227 }
228 expectedPSPMap := make(map[string]*v1beta1.PodSecurityPolicy)
229 for _, psp := range builtinPSPs {
230 expectedPSPMap[psp.Name] = psp
231 }
232 for pspName, psp := range expectedPSPMap {
233 if _, ok := availablePSPMap[pspName]; !ok {
234 if _, err := pspClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
235 return err
236 }
237 }
238 }
239 for pspName, _ := range availablePSPMap {
240 if _, ok := expectedPSPMap[pspName]; !ok {
241 if err := pspClient.Delete(ctx, pspName, metav1.DeleteOptions{}); err != nil {
242 return err
243 }
244 }
245 }
246 return nil
247}
248
249func reconcileClusterRoles(ctx context.Context, clientset *kubernetes.Clientset) error {
250 crClient := clientset.RbacV1().ClusterRoles()
251 availableCRs, err := crClient.List(ctx, metav1.ListOptions{
252 LabelSelector: "smalltown.com/builtin=true",
253 })
254 if err != nil {
255 return err
256 }
257 availableCRMap := make(map[string]struct{})
258 for _, cr := range availableCRs.Items {
259 availableCRMap[cr.Name] = struct{}{}
260 }
261 expectedCRMap := make(map[string]*rbacv1.ClusterRole)
262 for _, cr := range builtinClusterRoles {
263 expectedCRMap[cr.Name] = cr
264 }
265 for crName, psp := range expectedCRMap {
266 if _, ok := availableCRMap[crName]; !ok {
267 if _, err := crClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
268 return err
269 }
270 }
271 }
272 for crName, _ := range availableCRMap {
273 if _, ok := expectedCRMap[crName]; !ok {
274 if err := crClient.Delete(ctx, crName, metav1.DeleteOptions{}); err != nil {
275 return err
276 }
277 }
278 }
279 return nil
280}
281
282func reconcileClusterRoleBindings(ctx context.Context, clientset *kubernetes.Clientset) error {
283 crbClient := clientset.RbacV1().ClusterRoleBindings()
284 availableCRBs, err := crbClient.List(ctx, metav1.ListOptions{
285 LabelSelector: "smalltown.com/builtin=true",
286 })
287 if err != nil {
288 return err
289 }
290 availableCRBMap := make(map[string]struct{})
291 for _, crb := range availableCRBs.Items {
292 availableCRBMap[crb.Name] = struct{}{}
293 }
294 expectedCRBMap := make(map[string]*rbacv1.ClusterRoleBinding)
295 for _, crb := range builtinClusterRoleBindings {
296 expectedCRBMap[crb.Name] = crb
297 }
298 for crbName, psp := range expectedCRBMap {
299 if _, ok := availableCRBMap[crbName]; !ok {
300 if _, err := crbClient.Create(ctx, psp, metav1.CreateOptions{}); err != nil {
301 return err
302 }
303 }
304 }
305 for crbName, _ := range availableCRBMap {
306 if _, ok := expectedCRBMap[crbName]; !ok {
307 if err := crbClient.Delete(ctx, crbName, metav1.DeleteOptions{}); err != nil {
308 return err
309 }
310 }
311 }
312 return nil
313}