Skip to content

Commit b601c17

Browse files
Merge pull request #363 from JoelSpeed/prevent-node-daemon-escalation
OCPBUGS-22190: Add Node Admission restriction for Azure node Manager
2 parents 42eda7a + 061d984 commit b601c17

File tree

8 files changed

+145
-7
lines changed

8 files changed

+145
-7
lines changed

cmd/cluster-cloud-controller-manager-operator/main.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
"k8s.io/component-base/config"
3636
"k8s.io/component-base/config/options"
3737
"k8s.io/klog/v2"
38-
"k8s.io/klog/v2/textlogger"
3938
ctrl "sigs.k8s.io/controller-runtime"
4039
"sigs.k8s.io/controller-runtime/pkg/cache"
4140
"sigs.k8s.io/controller-runtime/pkg/healthz"
@@ -77,8 +76,7 @@ func init() {
7776
}
7877

7978
func main() {
80-
textLoggerCfg := textlogger.NewConfig()
81-
textLoggerCfg.AddFlags(flag.CommandLine)
79+
klog.InitFlags(flag.CommandLine)
8280

8381
metricsAddr := flag.String(
8482
"metrics-bind-address",
@@ -109,7 +107,7 @@ func main() {
109107
options.BindLeaderElectionFlags(&leaderElectionConfig, pflag.CommandLine)
110108
pflag.Parse()
111109

112-
ctrl.SetLogger(textlogger.NewLogger(textLoggerCfg).WithName("CCMOperator"))
110+
ctrl.SetLogger(klog.NewKlogr().WithName("CCMOperator"))
113111

114112
restConfig := ctrl.GetConfigOrDie()
115113
le := util.GetLeaderElectionDefaults(restConfig, configv1.LeaderElection{

manifests/0000_26_cloud-controller-manager-operator_02_rbac_operator.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ rules:
7575
- update
7676
- patch
7777

78+
- apiGroups:
79+
- admissionregistration.k8s.io
80+
resources:
81+
- validatingadmissionpolicies
82+
- validatingadmissionpolicybindings
83+
verbs:
84+
- get
85+
- list
86+
- watch
87+
- create
88+
- update
89+
- patch
90+
7891
# vSphere has a separate node manager that uses the service account kube-system/vsphere-cloud-controller-manager.
7992
# The operator must have these permissions to then grant them to the vSphere node manager.
8093
- apiGroups:
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingAdmissionPolicyBinding
3+
metadata:
4+
name: openshift-cloud-controller-manager-cloud-provider-azure-node-admission
5+
spec:
6+
policyName: openshift-cloud-controller-manager-cloud-provider-azure-node-admission
7+
validationActions: ["Deny"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: admissionregistration.k8s.io/v1beta1
2+
kind: ValidatingAdmissionPolicy
3+
metadata:
4+
name: openshift-cloud-controller-manager-cloud-provider-azure-node-admission
5+
spec:
6+
failurePolicy: Fail
7+
matchConstraints:
8+
resourceRules:
9+
- apiGroups: [""]
10+
apiVersions: ["v1"]
11+
operations: ["UPDATE"]
12+
resources: ["nodes"]
13+
validations:
14+
# all requests should have a node-name claim, this prevents impersonation of the SA.
15+
- expression: "has(request.userInfo.extra) && ('authentication.kubernetes.io/node-name' in request.userInfo.extra)"
16+
message: "this user must have a \"authentication.kubernetes.io/node-name\" claim"
17+
# all requests should originate from the MCN owner's node
18+
- expression: "object.metadata.name == request.userInfo.extra[\"authentication.kubernetes.io/node-name\"][0]"
19+
messageExpression: "'updates to Node ' + string(object.metadata.name) + ' may only be effected from the cloud node manager running on the same node'"
20+
matchConditions:
21+
# Only check requests from Azure Cloud Node Manager SA, this allows all other SAs with the correct RBAC to modify Nodes.
22+
- name: "check-only-machine-config-daemon-requests"
23+
expression: "request.userInfo.username == 'system:serviceaccount:openshift-cloud-controller-manager:cloud-node-manager'"

pkg/cloud/azure/azure.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/asaskevich/govalidator"
1111
configv1 "github.com/openshift/api/config/v1"
12+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
1213
appsv1 "k8s.io/api/apps/v1"
1314
rbacv1 "k8s.io/api/rbac/v1"
1415
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -31,6 +32,8 @@ var (
3132
{ReferenceObject: &appsv1.DaemonSet{}, EmbedFsPath: "assets/cloud-node-manager-daemonset.yaml"},
3233
{ReferenceObject: &rbacv1.ClusterRole{}, EmbedFsPath: "assets/azure-cloud-controller-manager-clusterrole.yaml"},
3334
{ReferenceObject: &rbacv1.ClusterRoleBinding{}, EmbedFsPath: "assets/azure-cloud-controller-manager-clusterrolebinding.yaml"},
35+
{ReferenceObject: &admissionregistrationv1.ValidatingAdmissionPolicy{}, EmbedFsPath: "assets/validating-admission-policy.yaml"},
36+
{ReferenceObject: &admissionregistrationv1.ValidatingAdmissionPolicyBinding{}, EmbedFsPath: "assets/validating-admission-policy-binding.yaml"},
3437
}
3538
)
3639

pkg/cloud/azure/azure_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func TestResourcesRenderingSmoke(t *testing.T) {
9090
}
9191

9292
resources := assets.GetRenderedResources()
93-
assert.Len(t, resources, 4)
93+
assert.Len(t, resources, 6)
9494
})
9595
}
9696
}

pkg/cloud/cloud_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,24 +149,28 @@ func TestGetResources(t *testing.T) {
149149
}, {
150150
name: "Azure resources returned as expected",
151151
testPlatform: platformsMap[string(configv1.AzurePlatformType)],
152-
expectedResourceCount: 5,
152+
expectedResourceCount: 7,
153153
expectedResourcesKindName: []string{
154154
"Deployment/azure-cloud-controller-manager",
155155
"DaemonSet/azure-cloud-node-manager",
156156
"ClusterRole/azure-cloud-controller-manager",
157157
"ClusterRoleBinding/cloud-controller-manager:azure-cloud-controller-manager",
158+
"ValidatingAdmissionPolicy/openshift-cloud-controller-manager-cloud-provider-azure-node-admission",
159+
"ValidatingAdmissionPolicyBinding/openshift-cloud-controller-manager-cloud-provider-azure-node-admission",
158160
"PodDisruptionBudget/azure-cloud-controller-manager",
159161
},
160162
}, {
161163
name: "Azure resources returned as expected with single node cluster",
162164
testPlatform: platformsMap[string(configv1.AzurePlatformType)],
163-
expectedResourceCount: 4,
165+
expectedResourceCount: 6,
164166
singleReplica: true,
165167
expectedResourcesKindName: []string{
166168
"Deployment/azure-cloud-controller-manager",
167169
"DaemonSet/azure-cloud-node-manager",
168170
"ClusterRole/azure-cloud-controller-manager",
169171
"ClusterRoleBinding/cloud-controller-manager:azure-cloud-controller-manager",
172+
"ValidatingAdmissionPolicy/openshift-cloud-controller-manager-cloud-provider-azure-node-admission",
173+
"ValidatingAdmissionPolicyBinding/openshift-cloud-controller-manager-cloud-provider-azure-node-admission",
170174
},
171175
}, {
172176
name: "Azure Stack resources returned as expected",

pkg/controllers/resourceapply/resourceapply.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"reflect"
1010

11+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
1112
appsv1 "k8s.io/api/apps/v1"
1213
corev1 "k8s.io/api/core/v1"
1314
policyv1 "k8s.io/api/policy/v1"
@@ -22,6 +23,7 @@ import (
2223
"sigs.k8s.io/controller-runtime/pkg/client"
2324
coreclientv1 "sigs.k8s.io/controller-runtime/pkg/client"
2425

26+
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
2527
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
2628
)
2729

@@ -83,6 +85,10 @@ func ApplyResource(ctx context.Context, client coreclientv1.Client, recorder rec
8385
return applyRoleBinding(ctx, client, recorder, t)
8486
case *rbacv1.ClusterRoleBinding:
8587
return applyClusterRoleBinding(ctx, client, recorder, t)
88+
case *admissionregistrationv1.ValidatingAdmissionPolicy:
89+
return applyValidatingAdmissionPolicy(ctx, client, recorder, t)
90+
case *admissionregistrationv1.ValidatingAdmissionPolicyBinding:
91+
return applyValidatingAdmissionPolicyBinding(ctx, client, recorder, t)
8692
default:
8793
return false, fmt.Errorf("unhandled type %T", resource)
8894
}
@@ -560,3 +566,87 @@ func applyClusterRoleBinding(ctx context.Context, client coreclientv1.Client, re
560566
recorder.Event(required, corev1.EventTypeNormal, ResourceUpdateSuccessEvent, "Resource was successfully updated")
561567
return true, nil
562568
}
569+
570+
func applyValidatingAdmissionPolicy(ctx context.Context, client coreclientv1.Client, recorder record.EventRecorder,
571+
requiredOriginal *admissionregistrationv1.ValidatingAdmissionPolicy) (bool, error) {
572+
required := requiredOriginal.DeepCopy()
573+
574+
existing := &admissionregistrationv1.ValidatingAdmissionPolicy{}
575+
err := client.Get(ctx, coreclientv1.ObjectKeyFromObject(requiredOriginal), existing)
576+
if apierrors.IsNotFound(err) {
577+
required := requiredOriginal.DeepCopy()
578+
if err := client.Create(ctx, required); err != nil {
579+
recorder.Event(required, corev1.EventTypeWarning, ResourceCreateFailedEvent, err.Error())
580+
return false, fmt.Errorf("validatingadmissionpolicy creation failed: %v", err)
581+
}
582+
recorder.Event(required, corev1.EventTypeNormal, ResourceCreateSuccessEvent, "Resource was successfully created")
583+
return true, nil
584+
} else if err != nil {
585+
recorder.Event(required, corev1.EventTypeWarning, ResourceUpdateFailedEvent, err.Error())
586+
return false, fmt.Errorf("failed to get validatingadmissionpolicy for update: %v", err)
587+
}
588+
589+
modified := false
590+
existingCopy := existing.DeepCopy()
591+
592+
resourcemerge.EnsureObjectMeta(&modified, &existingCopy.ObjectMeta, required.ObjectMeta)
593+
specEquivalent := equality.Semantic.DeepDerivative(required.Spec, existingCopy.Spec)
594+
if specEquivalent && !modified {
595+
return false, nil
596+
}
597+
// at this point we know that we're going to perform a write. We're just trying to get the object correct
598+
toWrite := existingCopy // shallow copy so the code reads easier
599+
toWrite.Spec = required.Spec
600+
601+
klog.V(2).Infof("ValidatingAdmissionPolicyConfiguration %q changes: %v", required.GetNamespace()+"/"+required.GetName(), resourceapply.JSONPatchNoError(existing, toWrite))
602+
603+
if err := client.Update(ctx, existingCopy); err != nil {
604+
recorder.Event(required, corev1.EventTypeWarning, ResourceUpdateFailedEvent, err.Error())
605+
return false, err
606+
}
607+
recorder.Event(required, corev1.EventTypeNormal, ResourceUpdateSuccessEvent, "Resource was successfully updated")
608+
609+
return true, nil
610+
}
611+
612+
func applyValidatingAdmissionPolicyBinding(ctx context.Context, client coreclientv1.Client, recorder record.EventRecorder,
613+
requiredOriginal *admissionregistrationv1.ValidatingAdmissionPolicyBinding) (bool, error) {
614+
required := requiredOriginal.DeepCopy()
615+
616+
existing := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{}
617+
err := client.Get(ctx, coreclientv1.ObjectKeyFromObject(requiredOriginal), existing)
618+
if apierrors.IsNotFound(err) {
619+
required := requiredOriginal.DeepCopy()
620+
if err := client.Create(ctx, required); err != nil {
621+
recorder.Event(required, corev1.EventTypeWarning, ResourceCreateFailedEvent, err.Error())
622+
return false, fmt.Errorf("validatingadmissionpolicybinding creation failed: %v", err)
623+
}
624+
recorder.Event(required, corev1.EventTypeNormal, ResourceCreateSuccessEvent, "Resource was successfully created")
625+
return true, nil
626+
} else if err != nil {
627+
recorder.Event(required, corev1.EventTypeWarning, ResourceUpdateFailedEvent, err.Error())
628+
return false, fmt.Errorf("failed to get validatingadmissionpolicybinding for update: %v", err)
629+
}
630+
631+
modified := false
632+
existingCopy := existing.DeepCopy()
633+
634+
resourcemerge.EnsureObjectMeta(&modified, &existingCopy.ObjectMeta, required.ObjectMeta)
635+
specEquivalent := equality.Semantic.DeepDerivative(required.Spec, existingCopy.Spec)
636+
if specEquivalent && !modified {
637+
return false, nil
638+
}
639+
// at this point we know that we're going to perform a write. We're just trying to get the object correct
640+
toWrite := existingCopy // shallow copy so the code reads easier
641+
toWrite.Spec = required.Spec
642+
643+
klog.V(2).Infof("ValidatingAdmissionPolicyBindingConfiguration %q changes: %v", required.GetNamespace()+"/"+required.GetName(), resourceapply.JSONPatchNoError(existing, toWrite))
644+
645+
if err := client.Update(ctx, existingCopy); err != nil {
646+
recorder.Event(required, corev1.EventTypeWarning, ResourceUpdateFailedEvent, err.Error())
647+
return false, err
648+
}
649+
recorder.Event(required, corev1.EventTypeNormal, ResourceUpdateSuccessEvent, "Resource was successfully updated")
650+
651+
return true, nil
652+
}

0 commit comments

Comments
 (0)