|
1 | 1 | package ingresscontroller
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "bytes" |
| 5 | + "context" |
4 | 6 | "fmt"
|
| 7 | + "net" |
5 | 8 | "net/http"
|
| 9 | + "os" |
6 | 10 | "slices"
|
7 | 11 | "strings"
|
8 | 12 |
|
9 | 13 | operatorv1 "github.com/openshift/api/operator/v1"
|
| 14 | + "github.com/openshift/managed-cluster-validating-webhooks/pkg/k8sutil" |
10 | 15 | "github.com/openshift/managed-cluster-validating-webhooks/pkg/webhooks/utils"
|
| 16 | + "github.com/pkg/errors" |
| 17 | + admissionv1 "k8s.io/api/admission/v1" |
11 | 18 | admissionregv1 "k8s.io/api/admissionregistration/v1"
|
| 19 | + corev1 "k8s.io/api/core/v1" |
12 | 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
13 | 21 | "k8s.io/apimachinery/pkg/runtime"
|
14 |
| - admissionctl "sigs.k8s.io/controller-runtime/pkg/webhook/admission" |
15 |
| - |
| 22 | + "k8s.io/apimachinery/pkg/util/yaml" |
| 23 | + "sigs.k8s.io/controller-runtime/pkg/client" |
16 | 24 | logf "sigs.k8s.io/controller-runtime/pkg/log"
|
| 25 | + admissionctl "sigs.k8s.io/controller-runtime/pkg/webhook/admission" |
17 | 26 | )
|
18 | 27 |
|
19 | 28 | const (
|
20 | 29 | WebhookName string = "ingresscontroller-validation"
|
21 | 30 | docString string = `Managed OpenShift Customer may create IngressControllers without necessary taints. This can cause those workloads to be provisioned on master nodes.`
|
22 | 31 | legacyIngressSupportFeatureFlag = "ext-managed.openshift.io/legacy-ingress-support"
|
| 32 | + installConfigMap = "cluster-config-v1" |
| 33 | + installConfigNamespace = "kube-system" |
| 34 | + installConfigKeyName = "install-config" |
23 | 35 | )
|
24 | 36 |
|
25 | 37 | var (
|
|
42 | 54 | )
|
43 | 55 |
|
44 | 56 | type IngressControllerWebhook struct {
|
45 |
| - s runtime.Scheme |
| 57 | + s runtime.Scheme |
| 58 | + kubeClient client.Client |
| 59 | + // Allow caching install config and machineCIDR values... |
| 60 | + machineCIDRIP net.IP |
| 61 | + machineCIDRNet *net.IPNet |
| 62 | +} |
| 63 | + |
| 64 | +// installConfig struct to load the min values needed from the install-config data. |
| 65 | +// Alternative would be to vendor all of github.com/openshift/installer/pkg/types to import the proper struct. |
| 66 | +// Required values: machineCidr info, anything else we gather from this? hcp vs classic, privatelink info, etc? |
| 67 | +type installConfig struct { |
| 68 | + metav1.TypeMeta `json:",inline"` |
| 69 | + metav1.ObjectMeta `json:"metadata"` |
| 70 | + Networking struct { |
| 71 | + MachineCIDR string `json:"machineCIDR"` |
| 72 | + } `json:"networking"` |
46 | 73 | }
|
47 | 74 |
|
48 | 75 | // ObjectSelector implements Webhook interface
|
@@ -149,12 +176,151 @@ func (wh *IngressControllerWebhook) authorized(request admissionctl.Request) adm
|
149 | 176 | }
|
150 | 177 | }
|
151 | 178 |
|
152 |
| - ret = admissionctl.Allowed("IngressController operation is allowed") |
| 179 | + /* TODO: |
| 180 | + * 1) ONLY check for privatelink clusters? How? |
| 181 | + * 2) HCP vs Classic, etc.. ...any other cluster type this should apply to? Is this handled by the |
| 182 | + * HypershiftEnabled vs ClassicEnabled flags set in this module? Currently HCP disabled. |
| 183 | + * If HCP is to be enabled for allowed source ranges, should this part of a 2nd ingress validator to |
| 184 | + * allow separation of validations between cluster install types? Is there a run time method available |
| 185 | + * to this validator to determine classic vs hcp? |
| 186 | + * 3) What other specifics should be checked here for this cidr check to be applicable?m |
| 187 | + */ |
| 188 | + // Only check for machine cidr in allowed ranges if creating or updating resource... |
| 189 | + reqOp := request.AdmissionRequest.Operation |
| 190 | + if reqOp == admissionv1.Create || reqOp == admissionv1.Update { |
| 191 | + //TODO: Will these need to iterate over more than just the default IngressController config? |
| 192 | + if ic.ObjectMeta.Name == "default" && ic.ObjectMeta.Namespace == "openshift-ingress-operator" { |
| 193 | + ret := wh.checkAllowsMachineCIDR(ic.Spec.EndpointPublishingStrategy.LoadBalancer.AllowedSourceRanges) |
| 194 | + ret.UID = request.AdmissionRequest.UID |
| 195 | + if !ret.Allowed { |
| 196 | + log.Info("Error checking minimum AllowedSourceRange", "err", ret.AdmissionResponse.String()) |
| 197 | + } |
| 198 | + return ret |
| 199 | + } |
| 200 | + } |
| 201 | + log.Info("############# DEBUG LOG: IngressController operation is allowed ###########") |
| 202 | + ret = admissionctl.Allowed("IngressController operation is allowed, machineCIDR n/a") |
153 | 203 | ret.UID = request.AdmissionRequest.UID
|
154 | 204 |
|
155 | 205 | return ret
|
156 | 206 | }
|
157 | 207 |
|
| 208 | +func (wh *IngressControllerWebhook) getMachineCIDR() (net.IP, *net.IPNet, error) { |
| 209 | + if wh.machineCIDRIP == nil || wh.machineCIDRNet == nil { |
| 210 | + instConf, err := wh.getClusterConfig() |
| 211 | + if err != nil { |
| 212 | + log.Error(err, "Failed to fetch machineCIDR", "namespace", installConfigNamespace, "configmap", installConfigMap) |
| 213 | + return nil, nil, err |
| 214 | + } |
| 215 | + if instConf == nil { |
| 216 | + err := fmt.Errorf("can not fetch machineCIDR from empty '%s' install config", installConfigMap) |
| 217 | + log.Error(err, "getMachineCIDR failed to find CIDR value") |
| 218 | + return nil, nil, err |
| 219 | + } |
| 220 | + if len(instConf.Networking.MachineCIDR) <= 0 { |
| 221 | + err := fmt.Errorf("empty machineCIDR string value parsed from '%s' install config", installConfigMap) |
| 222 | + log.Error(err, "getMachineCIDR found empty CIDR value") |
| 223 | + return nil, nil, err |
| 224 | + } |
| 225 | + machIP, machNet, err := net.ParseCIDR(string(instConf.Networking.MachineCIDR)) |
| 226 | + if err != nil { |
| 227 | + log.Error(err, "err parsing machineCIDR into network cidr", "machineCIDR", string(instConf.Networking.MachineCIDR)) |
| 228 | + return nil, nil, err |
| 229 | + } |
| 230 | + if machIP == nil || machNet == nil { |
| 231 | + err := fmt.Errorf("failed to parse machineCIDR string:'%s' into network structures", string(instConf.Networking.MachineCIDR)) |
| 232 | + log.Error(err, "failed to parse install-config machineCIDR") |
| 233 | + return nil, nil, err |
| 234 | + } |
| 235 | + // Successfully fetched, parsed, and converted the machineCIDR string into net structures... |
| 236 | + wh.machineCIDRIP = machIP |
| 237 | + wh.machineCIDRNet = machNet |
| 238 | + } |
| 239 | + return wh.machineCIDRIP, wh.machineCIDRNet, nil |
| 240 | +} |
| 241 | + |
| 242 | +/* Fetch the install-config from the kube-system config map's data. |
| 243 | + * this requires proper role, rolebinding for this service account's get() request |
| 244 | + * to succeed. (see toplevel selectorsyncset). This config should not change during runtime so |
| 245 | + * this operation should cache this if possible. |
| 246 | + * TODO: Should it retry fetching the config if there are any failures/errors encountered while |
| 247 | + * parsing out the the desired values? |
| 248 | + */ |
| 249 | +func (wh *IngressControllerWebhook) getClusterConfig() (*installConfig, error) { |
| 250 | + var err error |
| 251 | + if wh.kubeClient == nil { |
| 252 | + wh.kubeClient, err = k8sutil.KubeClient(&wh.s) |
| 253 | + if err != nil { |
| 254 | + log.Error(err, "Fail creating KubeClient for IngressControllerWebhook") |
| 255 | + return nil, err |
| 256 | + } |
| 257 | + } |
| 258 | + clusterConfig := &corev1.ConfigMap{} |
| 259 | + err = wh.kubeClient.Get(context.Background(), client.ObjectKey{Name: installConfigMap, Namespace: installConfigNamespace}, clusterConfig) |
| 260 | + if err != nil { |
| 261 | + log.Error(err, "Failed to fetch configmap: 'cluster-config-v1' for cluster config") |
| 262 | + return nil, err |
| 263 | + } |
| 264 | + data, ok := clusterConfig.Data[installConfigKeyName] |
| 265 | + if !ok { |
| 266 | + return nil, fmt.Errorf("did not find key %s in configmap %s/%s", installConfigKeyName, installConfigNamespace, installConfigMap) |
| 267 | + } |
| 268 | + instConf := &installConfig{} |
| 269 | + |
| 270 | + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(data)), 4096) |
| 271 | + if err := decoder.Decode(instConf); err != nil { |
| 272 | + return nil, errors.Wrap(err, "failed to decode install config") |
| 273 | + } |
| 274 | + return instConf, nil |
| 275 | +} |
| 276 | + |
| 277 | +func (wh *IngressControllerWebhook) checkAllowsMachineCIDR(ipRanges []operatorv1.CIDR) admissionctl.Response { |
| 278 | + // https://docs.openshift.com/container-platform/4.13/networking/configuring_ingress_cluster_traffic/configuring-ingress-cluster-traffic-load-balancer-allowed-source-ranges.html |
| 279 | + // Note: From docs it appears a missing ASR value/attr allows all. However... |
| 280 | + // once ASR values have been added to an ingresscontroller, later deleting all the ASRs can expose an issue |
| 281 | + // where the IGC will remaining in progressing state indefinitely. |
| 282 | + // For now return Allowed with a warning? |
| 283 | + if ipRanges == nil || len(ipRanges) <= 0 { |
| 284 | + return admissionctl.Allowed("Allowing empty 'AllowedSourceRanges'. Populate this value if operator remains in 'progressing' state") |
| 285 | + } |
| 286 | + machIP, machNet, err := wh.getMachineCIDR() |
| 287 | + if err != nil { |
| 288 | + // This represents a fault in either the webhook itself, webhook permissions, or install config. |
| 289 | + // Might be nice to have an env var etc we can set to allow proceeding w/o the immediate need to roll new code? |
| 290 | + return admissionctl.Errored(http.StatusInternalServerError, err) |
| 291 | + } |
| 292 | + machNetSize, machNetBits := machNet.Mask.Size() |
| 293 | + log.Info("Checking AllowedSourceRanges", "MachineCIDR", fmt.Sprintf("%s/%d", machIP.String(), machNetSize), "NetBits", machNetBits, "AllowedSourceRanges", ipRanges) |
| 294 | + for _, OpV1CIDR := range ipRanges { |
| 295 | + // Clean up the operatorV1.CIDR value into trimmed CIDR 'a.b.c.d/x' string |
| 296 | + ASRstring := strings.TrimSpace(string(OpV1CIDR)) |
| 297 | + log.Info(fmt.Sprintf("Checking allowed source:'%s'", ASRstring)) |
| 298 | + if len(ASRstring) <= 0 { |
| 299 | + continue |
| 300 | + } |
| 301 | + // Parse the Allowed Source Range Cidr entry into network structures... |
| 302 | + _, ASRNet, err := net.ParseCIDR(ASRstring) |
| 303 | + if err != nil { |
| 304 | + log.Info(fmt.Sprintf("failed to parse AllowedSourceRanges value: '%s'. Err: %s", string(ASRstring), err)) |
| 305 | + return admissionctl.Errored(http.StatusBadRequest, fmt.Errorf("failed to parse AllowedSourceRanges value: '%s'. Err: %s", string(ASRstring), err)) |
| 306 | + } |
| 307 | + // First check if this AlloweSourceRange entry network contains the machine cidr ip... |
| 308 | + if !ASRNet.Contains(machIP) { |
| 309 | + log.Info(fmt.Sprintf("AllowedSourceRange:'%s' does not contain machine CIDR:'%s/%d'", ASRstring, machIP.String(), machNetSize)) |
| 310 | + continue |
| 311 | + } |
| 312 | + // Check if this AlloweSourceRange entry mask includes the network. |
| 313 | + ASRNetSize, ASRNetBits := ASRNet.Mask.Size() |
| 314 | + if machNetBits == ASRNetBits && ASRNetSize <= machNetSize { |
| 315 | + log.Info(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machIP.String(), machNetSize, ASRstring)) |
| 316 | + return admissionctl.Allowed(fmt.Sprintf("Found machineCidr:'%s/%d' within AllowedSourceRange:'%s'", machIP.String(), machNetSize, ASRstring)) |
| 317 | + //return admissionctl.Allowed("IngressController operation is allowed. Minimum AllowedSourceRanges are met.") |
| 318 | + } |
| 319 | + } |
| 320 | + log.Info(fmt.Sprintf("machineCidr:'%s/%d' not found within networks provided by AllowedSourceRanges:'%v'", machIP.String(), machNetSize, ipRanges)) |
| 321 | + return admissionctl.Denied(fmt.Sprintf("At least one AllowedSourceRange must allow machine cidr:'%s/%d'", machIP.String(), machNetSize)) |
| 322 | +} |
| 323 | + |
158 | 324 | // isAllowedUser checks if the user is allowed to perform the action
|
159 | 325 | func isAllowedUser(request admissionctl.Request) bool {
|
160 | 326 | log.Info(fmt.Sprintf("Checking username %s on whitelist", request.UserInfo.Username))
|
@@ -199,7 +365,13 @@ func (s *IngressControllerWebhook) HypershiftEnabled() bool { return false }
|
199 | 365 | // NewWebhook creates a new webhook
|
200 | 366 | func NewWebhook() *IngressControllerWebhook {
|
201 | 367 | scheme := runtime.NewScheme()
|
| 368 | + err := corev1.AddToScheme(scheme) |
| 369 | + if err != nil { |
| 370 | + log.Error(err, "Fail adding corev1 scheme to IngressControllerWebhook") |
| 371 | + os.Exit(1) |
| 372 | + } |
202 | 373 | return &IngressControllerWebhook{
|
203 |
| - s: *scheme, |
| 374 | + s: *scheme, |
| 375 | + kubeClient: nil, |
204 | 376 | }
|
205 | 377 | }
|
0 commit comments