IPAM controller service account granted unnecessary full access to Secrets
Description
Impact
IPAM is the IP address Manager for Cluster API Provider Metal3. The IPAM controller's ClusterRole granted full CRUD permissions (create, delete, get, list, patch, update, watch) on core/v1 Secrets. The controller never accesses Secrets during normal operation. If the controller pod were compromised (e.g. via supply chain attack or container escape), an attacker could leverage these excessive permissions to read, modify, or delete Secrets in the namespace, potentially exposing credentials and other sensitive data.
All users running ip-address-manager versions prior to the patched releases are affected.
Patches
Fixed in: - v1.11.7 - v1.12.4 - v1.13.0
Users should upgrade to the patched version for their release branch.
Workarounds
Manually remove the Secrets resource entry from the metal3-ipam-controller-manager-role ClusterRole:
# Remove this entire block from the ClusterRole
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
Resources
- https://github.com/metal3-io/ip-address-manager/pull/1355
- https://github.com/metal3-io/ip-address-manager/pull/1356 (backport to release-1.12)
- https://github.com/metal3-io/ip-address-manager/pull/1357 (backport to release-1.11)
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
IPAM controller in Cluster API Provider Metal3 prior to v1.11.7, v1.12.4, and v1.13.0 unnecessarily grants full CRUD on Secrets, enabling credential exposure if the pod is compromised.
Vulnerability
The IPAM (IP Address Manager) controller in Cluster API Provider Metal3 is granted a ClusterRole that includes full CRUD permissions (create, delete, get, list, patch, update, watch) on core/v1 Secrets [1][2]. The controller never accesses Secrets during normal operation, making this an example of excessive RBAC privileges [1]. All users running ip-address-manager versions prior to v1.11.7, v1.12.4, and v1.13.0 are affected [2].
Exploitation
An attacker must first compromise the IPAM controller pod, for example via a supply chain attack, container escape, or a separate vulnerability in the controller [2]. Once the pod is under the attacker's control, the existing RBAC permissions allow the attacker to perform any of the permitted CRUD operations on Secrets in the namespace [2]. No additional authentication steps are needed within the cluster after the pod is compromised because the controller's service account already holds the excessive permissions.
Impact
A successful attacker can read, modify, or delete Secrets in the namespace where the IPAM controller operates [2]. This may expose sensitive credentials, API keys, or other confidential data stored as Secrets, and could also lead to privilege escalation, service disruption, or lateral movement within the cluster [2]. The impact is limited by the attacker's prior need to compromise the pod, but the attack surface is real for any environment where pod isolation is insufficient.
Mitigation
Users should upgrade to the fixed versions v1.11.7, v1.12.4, or v1.13.0 as appropriate for their release branch [2]. As a workaround, administrators can manually remove the Secrets resource block from the metal3-ipam-controller-manager-role ClusterRole [2]. The fix removes the kubebuilder RBAC marker that granted the unnecessary permissions [1][3][4]. No EOL statement or KEV listing has been published for this CVE at the time of writing.
- :bug: remove unused RBAC permissions for secrets by tuminoid · Pull Request #1355 · metal3-io/ip-address-manager
- CVE-2026-47190 - GitHub Advisory Database
- :bug: remove unused RBAC permissions for secrets by tuminoid · Pull Request #1357 · metal3-io/ip-address-manager
- :bug: remove unused RBAC permissions for secrets by tuminoid · Pull Request #1356 · metal3-io/ip-address-manager
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2>= 1.12.0, <= 1.12.3+ 1 more
- (no CPE)range: >= 1.12.0, <= 1.12.3
- (no CPE)range: <1.11.7 || >=1.11.0 <1.12.4 || >=1.12.0 <1.13.0
Patches
3cb5e2bb2052fMerge pull request #1357 from Nordix/tuomo/manual-cherrypick-1355-release-1.11
3 files changed · +1 −28
config/rbac/role.yaml+0 −12 modified@@ -15,18 +15,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - authentication.k8s.io resources:
controllers/ippool_controller.go+1 −2 modified@@ -66,7 +66,6 @@ type IPPoolReconciler struct { // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile handles IPPool events. func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, rerr error) { @@ -175,7 +174,7 @@ func (r *IPPoolReconciler) reconcileDelete(ctx context.Context, ) (ctrl.Result, error) { allocationsNb, err := ipPoolMgr.UpdateAddresses(ctx) if err != nil { - return checkReconcileError(err, "Failed to delete the old secrets") + return checkReconcileError(err, "Failed to delete the old addresses") } if allocationsNb == 0 {
main.go+0 −14 modified@@ -29,9 +29,7 @@ import ( "github.com/metal3-io/ip-address-manager/ipam" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/tools/leaderelection/resourcelock" @@ -114,9 +112,6 @@ func main() { } } - req, _ := labels.NewRequirement(clusterv1beta1.ClusterNameLabel, selection.Exists, nil) - clusterSecretCacheSelector := labels.NewSelector().Add(*req) - restConfig := ctrl.GetConfigOrDie() restConfig.QPS = float32(restConfigQPS) restConfig.Burst = restConfigBurst @@ -130,20 +125,11 @@ func main() { Cache: cache.Options{ DefaultNamespaces: watchNamespaces, SyncPeriod: &syncPeriod, - ByObject: map[client.Object]cache.ByObject{ - // Note: Only Secrets with the cluster name label are cached. - // The default client of the manager won't use the cache for secrets at all (see Client.Cache.DisableFor). - // The cached secrets will only be used by the secretCachingClient we create below. - &corev1.Secret{}: { - Label: clusterSecretCacheSelector, - }, - }, }, Client: client.Options{ Cache: &client.CacheOptions{ DisableFor: []client.Object{ &corev1.ConfigMap{}, - &corev1.Secret{}, }, }, },
ed6571e24867Merge pull request #1356 from Nordix/tuomo/manual-cherrypick-1355-release-1.12
3 files changed · +1 −28
config/rbac/role.yaml+0 −12 modified@@ -15,18 +15,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - authentication.k8s.io resources:
controllers/ippool_controller.go+1 −2 modified@@ -66,7 +66,6 @@ type IPPoolReconciler struct { // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile handles IPPool events. func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, rerr error) { @@ -175,7 +174,7 @@ func (r *IPPoolReconciler) reconcileDelete(ctx context.Context, ) (ctrl.Result, error) { allocationsNb, err := ipPoolMgr.UpdateAddresses(ctx) if err != nil { - return checkReconcileError(err, "Failed to delete the old secrets") + return checkReconcileError(err, "Failed to delete the old addresses") } if allocationsNb == 0 {
main.go+0 −14 modified@@ -29,9 +29,7 @@ import ( "github.com/metal3-io/ip-address-manager/ipam" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/tools/leaderelection/resourcelock" @@ -114,9 +112,6 @@ func main() { } } - req, _ := labels.NewRequirement(clusterv1beta1.ClusterNameLabel, selection.Exists, nil) - clusterSecretCacheSelector := labels.NewSelector().Add(*req) - restConfig := ctrl.GetConfigOrDie() restConfig.QPS = float32(restConfigQPS) restConfig.Burst = restConfigBurst @@ -130,20 +125,11 @@ func main() { Cache: cache.Options{ DefaultNamespaces: watchNamespaces, SyncPeriod: &syncPeriod, - ByObject: map[client.Object]cache.ByObject{ - // Note: Only Secrets with the cluster name label are cached. - // The default client of the manager won't use the cache for secrets at all (see Client.Cache.DisableFor). - // The cached secrets will only be used by the secretCachingClient we create below. - &corev1.Secret{}: { - Label: clusterSecretCacheSelector, - }, - }, }, Client: client.Options{ Cache: &client.CacheOptions{ DisableFor: []client.Object{ &corev1.ConfigMap{}, - &corev1.Secret{}, }, }, },
0199d7b97cb5Merge pull request #1355 from Nordix/tuomo/remove-extra-rbac-privileges
3 files changed · +1 −28
config/rbac/role.yaml+0 −12 modified@@ -15,18 +15,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - authentication.k8s.io resources:
controllers/ippool_controller.go+1 −2 modified@@ -66,7 +66,6 @@ type IPPoolReconciler struct { // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete // Reconcile handles IPPool events. func (r *IPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, rerr error) { @@ -175,7 +174,7 @@ func (r *IPPoolReconciler) reconcileDelete(ctx context.Context, ) (ctrl.Result, error) { allocationsNb, err := ipPoolMgr.UpdateAddresses(ctx) if err != nil { - return checkReconcileError(err, "Failed to delete the old secrets") + return checkReconcileError(err, "Failed to delete the old addresses") } if allocationsNb == 0 {
main.go+0 −14 modified@@ -29,9 +29,7 @@ import ( "github.com/metal3-io/ip-address-manager/ipam" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/tools/leaderelection/resourcelock" @@ -110,9 +108,6 @@ func main() { } } - req, _ := labels.NewRequirement(clusterv1.ClusterNameLabel, selection.Exists, nil) - clusterSecretCacheSelector := labels.NewSelector().Add(*req) - restConfig := ctrl.GetConfigOrDie() restConfig.QPS = float32(restConfigQPS) restConfig.Burst = restConfigBurst @@ -126,20 +121,11 @@ func main() { Cache: cache.Options{ DefaultNamespaces: watchNamespaces, SyncPeriod: &syncPeriod, - ByObject: map[client.Object]cache.ByObject{ - // Note: Only Secrets with the cluster name label are cached. - // The default client of the manager won't use the cache for secrets at all (see Client.Cache.DisableFor). - // The cached secrets will only be used by the secretCachingClient we create below. - &corev1.Secret{}: { - Label: clusterSecretCacheSelector, - }, - }, }, Client: client.Options{ Cache: &client.CacheOptions{ DisableFor: []client.Object{ &corev1.ConfigMap{}, - &corev1.Secret{}, }, }, },
Vulnerability mechanics
Root cause
"Excessive RBAC permissions: the controller's ClusterRole granted full CRUD access on core/v1 Secrets that the controller never needs during normal operation."
Attack vector
An attacker who has already compromised the ip-address-manager controller pod (e.g. via supply chain attack or container escape) can leverage the excessive RBAC permissions to read, modify, or delete any Secret in the namespace. The controller's ClusterRole granted full CRUD verbs on core/v1 Secrets, which the controller never actually uses during normal operation. This is a privilege escalation within the cluster: the attacker uses the pod's service account token to call the Kubernetes API with permissions far beyond what the controller requires, potentially exposing credentials and other sensitive data stored in Secrets.
Affected code
The vulnerability is in the RBAC configuration defined in `config/rbac/role.yaml` and the corresponding kubebuilder annotation in `controllers/ippool_controller.go`. The ClusterRole granted full CRUD permissions (create, delete, get, list, patch, update, watch) on core/v1 Secrets, even though the controller never accesses Secrets during normal operation. The patch removes the entire Secrets RBAC block from `config/rbac/role.yaml` and deletes the `// +kubebuilder:rbac:groups="",resources=secrets,...` annotation from `controllers/ippool_controller.go` [patch_id=3105284][patch_id=3105285][patch_id=3105286].
What the fix does
The patch removes the entire Secrets RBAC rule block from `config/rbac/role.yaml` (12 lines deleted) and deletes the corresponding `// +kubebuilder:rbac` annotation from `controllers/ippool_controller.go` [patch_id=3105284][patch_id=3105285][patch_id=3105286]. It also removes unused secret-caching code from `main.go`, including the label selector for caching Secrets and the `DisableFor` entry that excluded Secrets from the default client cache. A minor error message fix in `ippool_controller.go` changes "Failed to delete the old secrets" to "Failed to delete the old addresses" since the controller no longer handles secrets. These changes ensure the controller's service account has zero permissions on Secrets, closing the unnecessary privilege escalation path.
Preconditions
- authAttacker must have already compromised the ip-address-manager controller pod (e.g. via supply chain attack or container escape)
- configThe controller's service account must be bound to the over-privileged ClusterRole (default deployment)
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.