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