Repository: keel-hq/keel

Issue:
Missing permissions to create secret in Namespace under default clusterrole
Missing permissions to create secret in Namespace under default clusterrole

When installed keel via Helm chart , it create default cluster role with default permissions.
Now when keel triggers the helm upgrade, it throws error cannot create resource \"secrets\".

Default permissions for secrets resource are as below:

- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - get
  - watch
  - list

Below error was seen:

time="2025-07-15T17:57:05Z" level=info msg="performing update for service1-keel"
time="2025-07-15T17:57:06Z" level=info msg="creating upgraded release for service1-keel"
time="2025-07-15T17:57:06Z" level=fatal msg="helm3: failed to update release from chart" error="create: failed to create: secrets is forbidden: User \"system:serviceaccount:ns-operator:keel\" cannot create resource \"secrets\" in API group \"\" in the namespace \"ns-operator\""


After updating the cluster role for required permissions on secret resource then it started complaining about service resource.
Service resource also needs permission.
Below error was seen during failure for service resource:

time="2025-07-15T18:17:50Z" level=info msg="creating upgraded release for service1-keel"
time="2025-07-15T18:17:50Z" level=info msg="checking 2 resources for changes"
time="2025-07-15T18:17:50Z" level=info msg="warning: Upgrade \"service1-keel\" failed: could not get information about the resource: services \"service1\" is forbidden: User \"system:serviceaccount:ns-operator:keel\" cannot get resource \"services\" in API group \"\" in the namespace \"ns-operator\""
time="2025-07-15T18:17:50Z" level=fatal msg="helm3: failed to update release from chart" error="could not get information about the resource: services \"service1\" is forbidden: User \"system:serviceaccount:ns-operator:keel\" cannot get resource \"services\" in API group \"\" in the namespace \"ns-operator\""


Then I added below permissions for secret and service resources that made helm upgrade successful.

- apiGroups:
  - ""
  resources:
  - secrets
  - services
  verbs:
  - get
  - watch
  - list
  - create
  - update


Discussion:

Solution patch:
diff --git a/chart/keel/templates/clusterrole.yaml b/chart/keel/templates/clusterrole.yaml
index 76661082..d217e87e 100644
--- a/chart/keel/templates/clusterrole.yaml
+++ b/chart/keel/templates/clusterrole.yaml
@@ -1,51 +1,14 @@
 {{- if .Values.rbac.enabled }}
+{{- if .Values.rbac.clusterRole.create }}
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
   name: {{ template "keel.name" . }}
 rules:
-  - apiGroups:
-      - ""
-    resources:
-      - namespaces
-    verbs:
-      - watch
-      - list
-  - apiGroups:
-      - ""
-    resources:
-      - secrets
-    verbs:
-      - get
-      - watch
-      - list
-  - apiGroups:
-      - ""
-      - extensions
-      - apps
-      - batch
-    resources:
-      - pods
-      - replicasets
-      - replicationcontrollers
-      - statefulsets
-      - deployments
-      - daemonsets
-      - jobs
-      - cronjobs
-    verbs:
-      - get
-      - delete # required to delete pods during force upgrade of the same tag
-      - watch
-      - list
-      - update
-  - apiGroups:
-      - ""
-    resources:
-      - configmaps
-      - pods/portforward
-    verbs:
-      - get
-      - create
-      - update
+{{- range .Values.rbac.clusterRole.rules }}
+  - apiGroups: {{ .apiGroups | toYaml | nindent 6 }}
+    resources: {{ .resources | toYaml | nindent 6 }}
+    verbs: {{ .verbs | toYaml | nindent 6 }}
+{{- end }}
+{{ end }}
 {{ end }}
diff --git a/chart/keel/templates/clusterrolebinding.yaml b/chart/keel/templates/clusterrolebinding.yaml
index 54e1a849..2e7ad2a5 100644
--- a/chart/keel/templates/clusterrolebinding.yaml
+++ b/chart/keel/templates/clusterrolebinding.yaml
@@ -1,4 +1,5 @@
 {{- if .Values.rbac.enabled }}
+{{- if .Values.rbac.clusterRole.create }}
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
@@ -12,3 +13,4 @@ subjects:
     name: {{ template "serviceAccount.name" . }}
     namespace: {{ .Release.Namespace }}
 {{ end }}
+{{ end }}
diff --git a/chart/keel/templates/role.yaml b/chart/keel/templates/role.yaml
new file mode 100644
index 00000000..7b3fccdc
--- /dev/null
+++ b/chart/keel/templates/role.yaml
@@ -0,0 +1,15 @@
+{{- if .Values.rbac.enabled }}
+{{- if .Values.rbac.role.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ template "keel.name" . }}
+  namespace: {{ .Release.Namespace }}
+rules:
+{{- range .Values.rbac.role.rules }}
+  - apiGroups: {{ .apiGroups | toYaml | nindent 6 }}
+    resources: {{ .resources | toYaml | nindent 6 }}
+    verbs: {{ .verbs | toYaml | nindent 6 }}
+{{- end }}
+{{ end }}
+{{ end }}
diff --git a/chart/keel/templates/rolebinding.yaml b/chart/keel/templates/rolebinding.yaml
new file mode 100644
index 00000000..c4f853cd
--- /dev/null
+++ b/chart/keel/templates/rolebinding.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.rbac.enabled }}
+{{- if .Values.rbac.role.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ template "keel.name" . }}
+  namespace: {{ .Release.Namespace }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ template "keel.name" . }}
+subjects:
+  - kind: ServiceAccount
+    name: {{ template "serviceAccount.name" . }}
+    namespace: {{ .Release.Namespace }}
+{{ end }}
+{{ end }}
diff --git a/chart/keel/values.yaml b/chart/keel/values.yaml
index 08c17e25..d6daa914 100644
--- a/chart/keel/values.yaml
+++ b/chart/keel/values.yaml
@@ -179,6 +179,56 @@ rbac:
     # If rbac.serviceAccount.name is not set, a new name for the service account is generated
     create: true
 
+  clusterRole:
+    create: true
+    rules:
+    - apiGroups:
+        - ""
+      resources:
+        - namespaces
+      verbs:
+        - watch
+        - list
+    - apiGroups:
+        - ""
+      resources:
+        - secrets
+      verbs:
+        - get
+        - watch
+        - list
+    - apiGroups:
+        - ""
+        - extensions
+        - apps
+        - batch
+      resources:
+        - pods
+        - replicasets
+        - replicationcontrollers
+        - statefulsets
+        - deployments
+        - daemonsets
+        - jobs
+        - cronjobs
+      verbs:
+        - get
+        - delete 
+        - watch
+        - list
+        - update
+    - apiGroups:
+        - ""
+      resources:
+        - configmaps
+        - pods/portforward
+      verbs:
+        - get
+        - create
+        - update
+  role:
+    create: false
+
 # Resources
 resources:
   limits:



Test patch:
diff --git a/registry/registry_test.go b/registry/registry_test.go
index 47a83199..6c6a2d34 100644
--- a/registry/registry_test.go
+++ b/registry/registry_test.go
@@ -321,11 +321,28 @@ var tagsResp = `{
 	]
   }`
 
+// func TestGetDockerHubManyTags(t *testing.T) {
+// 	client := docker.New("https://quay.io", "", "")
+// 	tags, err := client.Tags("coreos/prometheus-operator-app")
+// 	if err != nil {
+// 		t.Errorf("error while getting repo: %s", err)
+// 	}
+// 	fmt.Println(tags)
+// }
+
 func TestGetDockerHubManyTags(t *testing.T) {
-	client := docker.New("https://quay.io", "", "")
-	tags, err := client.Tags("coreos/prometheus-operator")
-	if err != nil {
-		t.Errorf("error while getting repo: %s", err)
-	}
-	fmt.Println(tags)
-}
+    // Use DockerHub instead of Quay.io, since Quay repos often require authentication now
+    client := docker.New("https://registry.hub.docker.com", "", "")
+
+    // DockerHub official images are under "library/..."
+    tags, err := client.Tags("library/nginx")
+    if err != nil {
+        t.Errorf("error while getting repo: %s", err)
+    }
+
+    if len(tags) == 0 {
+        t.Errorf("expected to get some tags for nginx, got none")
+    }
+
+    fmt.Println("nginx tags:", tags[:5]) // print just a few tags for sanity
+}
\ No newline at end of file