Skip to content

Commit 915b39c

Browse files
authored
Merge pull request #35 from xrstf/authz-webhook-config
✨ Allow to configure an authorization webhook
2 parents 8d4d66b + 8f16a2b commit 915b39c

File tree

16 files changed

+486
-79
lines changed

16 files changed

+486
-79
lines changed

config/crd/bases/operator.kcp.io_rootshards.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,36 @@ spec:
120120
type: string
121121
type: object
122122
type: object
123+
authorization:
124+
properties:
125+
webhook:
126+
properties:
127+
allowPaths:
128+
description: |-
129+
A list of HTTP paths to skip during authorization, i.e. these are authorized without contacting the 'core' kubernetes server.
130+
If specified, completely overwrites the default of [/healthz,/readyz,/livez].
131+
items:
132+
type: string
133+
type: array
134+
cacheAuthorizedTTL:
135+
description: The duration to cache 'authorized' responses
136+
from the webhook authorizer.
137+
type: string
138+
cacheUnauthorizedTTL:
139+
description: The duration to cache 'unauthorized' responses
140+
from the webhook authorizer.
141+
type: string
142+
configSecretName:
143+
description: |-
144+
Name of a Kubernetes Secret that contains a kubeconfig formatted file that defines the
145+
authorization webhook configuration.
146+
type: string
147+
version:
148+
description: The API version of the authorization.k8s.io SubjectAccessReview
149+
to send to and expect from the webhook.
150+
type: string
151+
type: object
152+
type: object
123153
cache:
124154
description: Cache configures the cache server (with a Kubernetes-like
125155
API) used by a sharded kcp instance.

config/crd/bases/operator.kcp.io_shards.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,36 @@ spec:
120120
type: string
121121
type: object
122122
type: object
123+
authorization:
124+
properties:
125+
webhook:
126+
properties:
127+
allowPaths:
128+
description: |-
129+
A list of HTTP paths to skip during authorization, i.e. these are authorized without contacting the 'core' kubernetes server.
130+
If specified, completely overwrites the default of [/healthz,/readyz,/livez].
131+
items:
132+
type: string
133+
type: array
134+
cacheAuthorizedTTL:
135+
description: The duration to cache 'authorized' responses
136+
from the webhook authorizer.
137+
type: string
138+
cacheUnauthorizedTTL:
139+
description: The duration to cache 'unauthorized' responses
140+
from the webhook authorizer.
141+
type: string
142+
configSecretName:
143+
description: |-
144+
Name of a Kubernetes Secret that contains a kubeconfig formatted file that defines the
145+
authorization webhook configuration.
146+
type: string
147+
version:
148+
description: The API version of the authorization.k8s.io SubjectAccessReview
149+
to send to and expect from the webhook.
150+
type: string
151+
type: object
152+
type: object
123153
clusterDomain:
124154
type: string
125155
etcd:

internal/resources/rootshard/deployment.go

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package rootshard
1818

1919
import (
2020
"fmt"
21-
"strings"
2221

2322
"k8c.io/reconciler/pkg/reconciling"
2423

@@ -79,23 +78,8 @@ func DeploymentReconciler(rootShard *operatorv1alpha1.RootShard) reconciling.Nam
7978
MountPath: getCAMountPath(operatorv1alpha1.RootCA),
8079
}}
8180

82-
image, _ := resources.GetImageSettings(rootShard.Spec.Image)
8381
args := getArgs(rootShard)
8482

85-
if rootShard.Spec.Etcd.TLSConfig != nil {
86-
secretMounts = append(secretMounts, utils.SecretMount{
87-
VolumeName: "etcd-client-cert",
88-
SecretName: rootShard.Spec.Etcd.TLSConfig.SecretRef.Name,
89-
MountPath: "/etc/etcd/tls",
90-
})
91-
92-
args = append(args,
93-
"--etcd-certfile=/etc/etcd/tls/tls.crt",
94-
"--etcd-keyfile=/etc/etcd/tls/tls.key",
95-
"--etcd-cafile=/etc/etcd/tls/ca.crt",
96-
)
97-
}
98-
9983
for _, cert := range []operatorv1alpha1.Certificate{
10084
// requires server CA and the logical-cluster-admin cert to be mounted
10185
operatorv1alpha1.LogicalClusterAdminCertificate,
@@ -146,7 +130,6 @@ func DeploymentReconciler(rootShard *operatorv1alpha1.RootShard) reconciling.Nam
146130

147131
dep.Spec.Template.Spec.Containers = []corev1.Container{{
148132
Name: ServerContainerName,
149-
Image: image,
150133
Command: []string{"/kcp", "start"},
151134
Args: args,
152135
VolumeMounts: volumeMounts,
@@ -158,19 +141,9 @@ func DeploymentReconciler(rootShard *operatorv1alpha1.RootShard) reconciling.Nam
158141
}}
159142
dep.Spec.Template.Spec.Volumes = volumes
160143

161-
// explicitly set the replicas if it is configured in the RootShard
162-
// object or if the existing Deployment object doesn't have replicas
163-
// configured. This will allow a HPA to interact with the replica
164-
// count.
165-
if rootShard.Spec.Replicas != nil {
166-
dep.Spec.Replicas = rootShard.Spec.Replicas
167-
} else if dep.Spec.Replicas == nil {
168-
dep.Spec.Replicas = ptr.To[int32](2)
169-
}
170-
171-
dep, err := utils.ApplyAuditConfiguration(dep, rootShard.Spec.Audit)
144+
dep, err := utils.ApplyCommonShardConfig(dep, &rootShard.Spec.CommonShardSpec)
172145
if err != nil {
173-
return nil, fmt.Errorf("failed to apply audit configuration: %w", err)
146+
return nil, fmt.Errorf("failed to shard configuration: %w", err)
174147
}
175148

176149
return dep, nil
@@ -196,9 +169,6 @@ func getArgs(rootShard *operatorv1alpha1.RootShard) []string {
196169
fmt.Sprintf("--service-account-key-file=%s/tls.crt", getCertificateMountPath(operatorv1alpha1.ServiceAccountCertificate)),
197170
fmt.Sprintf("--service-account-private-key-file=%s/tls.key", getCertificateMountPath(operatorv1alpha1.ServiceAccountCertificate)),
198171

199-
// Etcd client configuration.
200-
fmt.Sprintf("--etcd-servers=%s", strings.Join(rootShard.Spec.Etcd.Endpoints, ",")),
201-
202172
// General shard configuration.
203173
fmt.Sprintf("--shard-base-url=%s", resources.GetRootShardBaseURL(rootShard)),
204174
fmt.Sprintf("--shard-external-url=https://%s:%d", rootShard.Spec.External.Hostname, rootShard.Spec.External.Port),

internal/resources/shard/deployment.go

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package shard
1818

1919
import (
2020
"fmt"
21-
"strings"
2221

2322
"k8c.io/reconciler/pkg/reconciling"
2423

@@ -78,23 +77,8 @@ func DeploymentReconciler(shard *operatorv1alpha1.Shard, rootShard *operatorv1al
7877
MountPath: getCAMountPath(operatorv1alpha1.RootCA),
7978
}}
8079

81-
image, _ := resources.GetImageSettings(shard.Spec.Image)
8280
args := getArgs(shard, rootShard)
8381

84-
if shard.Spec.Etcd.TLSConfig != nil {
85-
secretMounts = append(secretMounts, utils.SecretMount{
86-
VolumeName: "etcd-client-cert",
87-
SecretName: rootShard.Spec.Etcd.TLSConfig.SecretRef.Name,
88-
MountPath: "/etc/etcd/tls",
89-
})
90-
91-
args = append(args,
92-
"--etcd-certfile=/etc/etcd/tls/tls.crt",
93-
"--etcd-keyfile=/etc/etcd/tls/tls.key",
94-
"--etcd-cafile=/etc/etcd/tls/ca.crt",
95-
)
96-
}
97-
9882
for _, cert := range []operatorv1alpha1.Certificate{
9983
// requires server CA and the shard client cert to be mounted
10084
operatorv1alpha1.ClientCertificate,
@@ -143,7 +127,6 @@ func DeploymentReconciler(shard *operatorv1alpha1.Shard, rootShard *operatorv1al
143127

144128
dep.Spec.Template.Spec.Containers = []corev1.Container{{
145129
Name: ServerContainerName,
146-
Image: image,
147130
Command: []string{"/kcp", "start"},
148131
Args: args,
149132
VolumeMounts: volumeMounts,
@@ -155,19 +138,9 @@ func DeploymentReconciler(shard *operatorv1alpha1.Shard, rootShard *operatorv1al
155138
}}
156139
dep.Spec.Template.Spec.Volumes = volumes
157140

158-
// explicitly set the replicas if it is configured in the RootShard
159-
// object or if the existing Deployment object doesn't have replicas
160-
// configured. This will allow a HPA to interact with the replica
161-
// count.
162-
if shard.Spec.Replicas != nil {
163-
dep.Spec.Replicas = shard.Spec.Replicas
164-
} else if dep.Spec.Replicas == nil {
165-
dep.Spec.Replicas = ptr.To[int32](2)
166-
}
167-
168-
dep, err := utils.ApplyAuditConfiguration(dep, shard.Spec.Audit)
141+
dep, err := utils.ApplyCommonShardConfig(dep, &shard.Spec.CommonShardSpec)
169142
if err != nil {
170-
return nil, fmt.Errorf("failed to apply audit configuration: %w", err)
143+
return nil, fmt.Errorf("failed to shard configuration: %w", err)
171144
}
172145

173146
return dep, nil
@@ -195,9 +168,6 @@ func getArgs(shard *operatorv1alpha1.Shard, rootShard *operatorv1alpha1.RootShar
195168
fmt.Sprintf("--shard-client-key-file=%s/tls.crt", getCertificateMountPath(operatorv1alpha1.ClientCertificate)),
196169
fmt.Sprintf("--shard-client-cert-file=%s/tls.key", getCertificateMountPath(operatorv1alpha1.ClientCertificate)),
197170

198-
// Etcd client configuration.
199-
fmt.Sprintf("--etcd-servers=%s", strings.Join(shard.Spec.Etcd.Endpoints, ",")),
200-
201171
// General shard configuration.
202172
fmt.Sprintf("--shard-name=%s", shard.Name),
203173
fmt.Sprintf("--external-hostname=%s", resources.GetShardBaseHost(shard)),

internal/resources/utils/audit.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ limitations under the License.
1717
package utils
1818

1919
import (
20-
"errors"
2120
"fmt"
2221

2322
appsv1 "k8s.io/api/apps/v1"
@@ -26,16 +25,12 @@ import (
2625
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
2726
)
2827

29-
func ApplyAuditConfiguration(deployment *appsv1.Deployment, config *operatorv1alpha1.AuditSpec) (*appsv1.Deployment, error) {
30-
if len(deployment.Spec.Template.Spec.Containers) == 0 {
31-
return deployment, errors.New("Deployment does not contain any containers")
32-
}
33-
28+
func applyAuditConfiguration(deployment *appsv1.Deployment, config *operatorv1alpha1.AuditSpec) *appsv1.Deployment {
3429
if config == nil || config.Webhook == nil {
35-
return deployment, nil
30+
return deployment
3631
}
3732

38-
return applyAuditWebhookConfiguration(deployment, *config.Webhook), nil
33+
return applyAuditWebhookConfiguration(deployment, *config.Webhook)
3934
}
4035

4136
func applyAuditWebhookConfiguration(deployment *appsv1.Deployment, config operatorv1alpha1.AuditWebhookSpec) *appsv1.Deployment {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
appsv1 "k8s.io/api/apps/v1"
24+
corev1 "k8s.io/api/core/v1"
25+
26+
operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1"
27+
)
28+
29+
func applyAuthorizationConfiguration(deployment *appsv1.Deployment, config *operatorv1alpha1.AuthorizationSpec) *appsv1.Deployment {
30+
if config == nil || config.Webhook == nil {
31+
return deployment
32+
}
33+
34+
return applyAuthorizationWebhookConfiguration(deployment, *config.Webhook)
35+
}
36+
37+
func applyAuthorizationWebhookConfiguration(deployment *appsv1.Deployment, config operatorv1alpha1.AuthorizationWebhookSpec) *appsv1.Deployment {
38+
podSpec := deployment.Spec.Template.Spec
39+
40+
var extraArgs []string
41+
42+
if vals := config.AllowPaths; len(vals) > 0 {
43+
extraArgs = append(extraArgs, fmt.Sprintf("--authorization-always-allow-paths=%s", strings.Join(vals, ",")))
44+
}
45+
46+
if val := config.CacheAuthorizedTTL; val != nil {
47+
extraArgs = append(extraArgs, fmt.Sprintf("--authorization-webhook-cache-authorized-ttl=%v", val.String()))
48+
}
49+
50+
if val := config.CacheUnauthorizedTTL; val != nil {
51+
extraArgs = append(extraArgs, fmt.Sprintf("--authorization-webhook-cache-unauthorized-ttl=%v", val.String()))
52+
}
53+
54+
if val := config.Version; val != "" {
55+
extraArgs = append(extraArgs, fmt.Sprintf("--authorization-webhook-version=%s", val))
56+
}
57+
58+
if val := config.ConfigSecretName; val != "" {
59+
volumeName := "authorization-webhook-config"
60+
mountPath := "/etc/kcp/authorization/webhook"
61+
62+
extraArgs = append(extraArgs, fmt.Sprintf("--authorization-webhook-config-file=%s/kubeconfig", mountPath))
63+
podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, corev1.VolumeMount{
64+
Name: volumeName,
65+
ReadOnly: true,
66+
MountPath: mountPath,
67+
})
68+
69+
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{
70+
Name: volumeName,
71+
VolumeSource: corev1.VolumeSource{
72+
Secret: &corev1.SecretVolumeSource{
73+
SecretName: val,
74+
},
75+
},
76+
})
77+
}
78+
79+
podSpec.Containers[0].Args = append(podSpec.Containers[0].Args, extraArgs...)
80+
deployment.Spec.Template.Spec = podSpec
81+
82+
return deployment
83+
}

0 commit comments

Comments
 (0)