Scope barman Secret RBAC

Avoid granting namespace-wide Secret access when ObjectStores do not need credential Secrets.

Ref: #892
Signed-off-by: Martin Hansen <dontbeevilpls@gmail.com>
This commit is contained in:
Martin Hansen 2026-05-07 15:48:27 +02:00
parent e550b48cd5
commit ea0a7b5698
No known key found for this signature in database
5 changed files with 76 additions and 18 deletions

View File

@ -60,7 +60,7 @@ func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRu
}
}
return []rbacv1.PolicyRule{
rules := []rbacv1.PolicyRule{
{
APIGroups: []string{
barmancloudv1.GroupVersion.Group,
@ -87,7 +87,11 @@ func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRu
},
ResourceNames: barmanObjectsSet.ToSortedList(),
},
{
}
secrets := secretsSet.ToSortedList()
if len(secrets) > 0 {
rules = append(rules, rbacv1.PolicyRule{
APIGroups: []string{
"",
},
@ -99,9 +103,11 @@ func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRu
"watch",
"list",
},
ResourceNames: secretsSet.ToSortedList(),
},
ResourceNames: secrets,
})
}
return rules
}
// ObjectStoreNamesFromRole extracts the ObjectStore names referenced

View File

@ -78,13 +78,12 @@ var _ = Describe("BuildRoleRules", func() {
Expect(rules[2].ResourceNames).To(ConsistOf("secret-a", "secret-b"))
})
It("should produce rules with empty ResourceNames for empty input", func() {
It("should not produce a secrets rule for empty input", func() {
rules := BuildRoleRules(nil)
Expect(rules).To(HaveLen(3))
Expect(rules).To(HaveLen(2))
Expect(rules[0].ResourceNames).To(BeEmpty())
Expect(rules[0].ResourceNames).NotTo(BeNil())
Expect(rules[1].ResourceNames).To(BeEmpty())
Expect(rules[2].ResourceNames).To(BeEmpty())
})
It("should deduplicate secret names across ObjectStores", func() {
@ -95,6 +94,31 @@ var _ = Describe("BuildRoleRules", func() {
rules := BuildRoleRules(objects)
Expect(rules[2].ResourceNames).To(Equal([]string{"shared-secret"}))
})
It("should not produce a secrets rule when ObjectStores use IAM role inheritance", func() {
objects := []barmancloudv1.ObjectStore{
{
ObjectMeta: metav1.ObjectMeta{
Name: "store-a",
Namespace: "default",
},
Spec: barmancloudv1.ObjectStoreSpec{
Configuration: barmanapi.BarmanObjectStoreConfiguration{
DestinationPath: "s3://bucket/path",
BarmanCredentials: barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
InheritFromIAMRole: true,
},
},
},
},
},
}
rules := BuildRoleRules(objects)
Expect(rules).To(HaveLen(2))
Expect(rules[0].ResourceNames).To(Equal([]string{"store-a"}))
Expect(rules[1].ResourceNames).To(Equal([]string{"store-a"}))
})
})
var _ = Describe("BuildRole", func() {

View File

@ -28,6 +28,9 @@ import (
func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCredentials) []string {
var references []*machineryapi.SecretKeySelector
if barmanCredentials.AWS != nil {
// When using IAM role inheritance, barman-cloud uses the pod
// environment credential chain and does not read credential Secrets.
if !barmanCredentials.AWS.InheritFromIAMRole {
references = append(
references,
barmanCredentials.AWS.AccessKeyIDReference,
@ -36,6 +39,7 @@ func CollectSecretNamesFromCredentials(barmanCredentials *barmanapi.BarmanCreden
barmanCredentials.AWS.SessionToken,
)
}
}
if barmanCredentials.Azure != nil {
// When using default Azure credentials or managed identity, no secrets are required
if !barmanCredentials.Azure.UseDefaultAzureCredentials &&

View File

@ -57,6 +57,29 @@ var _ = Describe("CollectSecretNamesFromCredentials", func() {
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(BeEmpty())
})
It("should return empty list when using InheritFromIAMRole", func() {
credentials := &barmanapi.BarmanCredentials{
AWS: &barmanapi.S3Credentials{
InheritFromIAMRole: true,
AccessKeyIDReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-secret",
},
Key: "access-key-id",
},
RegionReference: &machineryapi.SecretKeySelector{
LocalObjectReference: machineryapi.LocalObjectReference{
Name: "aws-region",
},
Key: "region",
},
},
}
secrets := CollectSecretNamesFromCredentials(credentials)
Expect(secrets).To(BeEmpty())
})
})
Context("when collecting secrets from Azure credentials", func() {

View File

@ -31,8 +31,8 @@ import (
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
@ -261,7 +261,7 @@ var _ = Describe("ObjectStoreReconciler", func() {
Expect(result).To(Equal(reconcile.Result{}))
})
It("should produce empty ResourceNames when all ObjectStores are deleted", func() {
It("should omit the secrets rule when all ObjectStores are deleted", func() {
store := newTestObjectStore("my-store", "default", "aws-creds")
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*store})
@ -291,10 +291,11 @@ var _ = Describe("ObjectStoreReconciler", func() {
Name: "my-cluster-barman-cloud",
}, &updatedRole)).To(Succeed())
// All rules should have empty ResourceNames
Expect(updatedRole.Rules[0].ResourceNames).To(BeEmpty())
Expect(updatedRole.Rules[1].ResourceNames).To(BeEmpty())
Expect(updatedRole.Rules[2].ResourceNames).To(BeEmpty())
for _, rule := range updatedRole.Rules {
Expect(rule.Resources).NotTo(Equal([]string{"secrets"}))
}
})
It("should return an error when listing Roles fails", func() {