Skip to content

Commit 947c60e

Browse files
committed
Make ServiceAccount optional with SyntheticPermissions fallback
Make the ServiceAccount field optional in the experimental ClusterExtension CRD and re-introduce the SyntheticPermissions feature with the intention of making in the recommended authentication mechanism. SyntheticPermissions provides better architectural alignment with the cluster-scoped ClusterExtension API compared to namespace-scoped ServiceAccount identities. When the SyntheticPermissions feature gate is enabled and a ServiceAccount is not specified on a ClusterExtension, the controller will use synthetic user and group impersonation instead of ServiceAccount-based authentication. ServiceAccount-based authentication remains available for users who prefer that pattern or have specific requirements for it. Key changes: - ServiceAccount field is now optional (experimental CRD only) - SyntheticPermissions feature gate enabled for experimental release - Refactored authentication package to support both approaches - Updated RBAC to allow impersonation for synthetic user pattern - Added documentation for using synthetic permissions Signed-off-by: Joe Lanford <[email protected]>
1 parent a6027b8 commit 947c60e

31 files changed

+691
-220
lines changed

api/v1/clusterextension_types.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ type ClusterExtensionSpec struct {
5555
// Some extensions may contain namespace-scoped resources to be applied in other namespaces.
5656
// This namespace must exist.
5757
//
58+
// <opcon:standard:description>
59+
// This is also the namespace of the referenced service account.
60+
// </opcon:standard:description>
61+
// <opcon:experimental:description>
62+
// This is also the namespace of the referenced service account, if specified.
63+
// </opcon:experimental:description>
64+
//
5865
// namespace is required, immutable, and follows the DNS label standard
5966
// as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-),
6067
// start and end with an alphanumeric character, and be no longer than 63 characters
@@ -69,11 +76,24 @@ type ClusterExtensionSpec struct {
6976

7077
// serviceAccount is a reference to a ServiceAccount used to perform all interactions
7178
// with the cluster that are required to manage the extension.
72-
// Appropriate RBAC must be configured to grant the necessary permissions to the ServiceAccount.
79+
// <opcon:standard:description>
80+
// The ServiceAccount must be configured with the necessary permissions to perform these interactions.
81+
//
7382
// serviceAccount is required.
83+
// </opcon:standard:description>
7484
//
75-
// +kubebuilder:validation:Required
76-
ServiceAccount ServiceAccountReference `json:"serviceAccount"`
85+
// <opcon:experimental:description>
86+
// If serviceAccount is specified, OLM will authenticate as that service account.
87+
// Otherwise, operator-controller will authenticate as:
88+
// - User: "olmv1:clusterextensions:<clusterExtensionName>"
89+
// - Group: "olmv1:clusterextensions"
90+
//
91+
// The authenticated user must be configured with the necessary permissions to perform these interactions.
92+
// </opcon:experimental:description>
93+
//
94+
// <opcon:standard:validation:Required>
95+
// <opcon:experimental:validation:Optional>
96+
ServiceAccount ServiceAccountReference `json:"serviceAccount,omitzero"`
7797

7898
// source is a required field which selects the installation source of content
7999
// for this ClusterExtension. Selection is performed by setting the sourceType.
@@ -373,7 +393,7 @@ type CatalogFilter struct {
373393
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
374394
}
375395

376-
// ServiceAccountReference identifies the serviceAccount name used for managing a ClusterExtension.
396+
// ServiceAccountReference identifies the serviceAccount name used to manage a ClusterExtension.
377397
type ServiceAccountReference struct {
378398
// name is a required, immutable reference to the name of the ServiceAccount
379399
// to be used for installation and management of the content for the package
@@ -401,7 +421,7 @@ type ServiceAccountReference struct {
401421
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable"
402422
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="name must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters"
403423
// +kubebuilder:validation:Required
404-
Name string `json:"name"`
424+
Name string `json:"name,omitempty"`
405425
}
406426

407427
// PreflightConfig holds the configuration for the preflight checks. If used, at least one preflight check must be non-nil.

cmd/operator-controller/main.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
k8slabels "k8s.io/apimachinery/pkg/labels"
3737
k8stypes "k8s.io/apimachinery/pkg/types"
3838
apimachineryrand "k8s.io/apimachinery/pkg/util/rand"
39+
"k8s.io/apiserver/pkg/authentication/user"
3940
"k8s.io/client-go/discovery"
4041
"k8s.io/client-go/discovery/cached/memory"
4142
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -63,6 +64,7 @@ import (
6364
ocv1 "github.com/operator-framework/operator-controller/api/v1"
6465
"github.com/operator-framework/operator-controller/internal/operator-controller/action"
6566
"github.com/operator-framework/operator-controller/internal/operator-controller/applier"
67+
"github.com/operator-framework/operator-controller/internal/operator-controller/authentication"
6668
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
6769
"github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/cache"
6870
catalogclient "github.com/operator-framework/operator-controller/internal/operator-controller/catalogmetadata/client"
@@ -645,6 +647,9 @@ func setupHelm(
645647
return fmt.Errorf("unable to create core client: %w", err)
646648
}
647649
clientRestConfigMapper := action.ServiceAccountRestConfigMapper()
650+
if features.OperatorControllerFeatureGate.Enabled(features.SyntheticPermissions) {
651+
clientRestConfigMapper = action.SyntheticUserRestConfigMapper(clientRestConfigMapper)
652+
}
648653

649654
cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(),
650655
helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)),
@@ -668,7 +673,16 @@ func setupHelm(
668673
// determine if PreAuthorizer should be enabled based on feature gate
669674
var preAuth authorization.PreAuthorizer
670675
if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) {
671-
preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient())
676+
preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient(), func(ext *ocv1.ClusterExtension) (*user.DefaultInfo, error) {
677+
if ext.Spec.ServiceAccount.Name == "" && features.OperatorControllerFeatureGate.Enabled(features.SyntheticPermissions) {
678+
syntheticConfig := authentication.SyntheticImpersonationConfig(*ext)
679+
return &user.DefaultInfo{Name: syntheticConfig.UserName, Groups: syntheticConfig.Groups}, nil
680+
} else if ext.Spec.ServiceAccount.Name == "" || ext.Spec.Namespace == "" {
681+
return nil, errors.New("service account name and namespace must be specified")
682+
}
683+
saConfig := authentication.ServiceAccountImpersonationConfig(*ext)
684+
return &user.DefaultInfo{Name: saConfig.UserName, Groups: saConfig.Groups}, nil
685+
})
672686
}
673687

674688
cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())

docs/api-reference/olmv1-api-reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ _Appears in:_
339339

340340
| Field | Description | Default | Validation |
341341
| --- | --- | --- | --- |
342-
| `namespace` _string_ | namespace is a reference to a Kubernetes namespace.<br />This designates the default namespace where namespace-scoped resources<br />for the extension are applied to the cluster.<br />Some extensions may contain namespace-scoped resources to be applied in other namespaces.<br />This namespace must exist.<br />namespace is required, immutable, and follows the DNS label standard<br />as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-),<br />start and end with an alphanumeric character, and be no longer than 63 characters<br />[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63 <br />Required: \{\} <br /> |
343-
| `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a reference to a ServiceAccount used to perform all interactions<br />with the cluster that are required to manage the extension.<br />Appropriate RBAC must be configured to grant the necessary permissions to the ServiceAccount.<br />serviceAccount is required. | | Required: \{\} <br /> |
342+
| `namespace` _string_ | namespace is a reference to a Kubernetes namespace.<br />This designates the default namespace where namespace-scoped resources<br />for the extension are applied to the cluster.<br />Some extensions may contain namespace-scoped resources to be applied in other namespaces.<br />This namespace must exist.<br /><opcon:standard:description><br />This is also the namespace of the referenced service account.<br /></opcon:standard:description><br /><opcon:experimental:description><br />This is also the namespace of the referenced service account, if specified.<br /></opcon:experimental:description><br />namespace is required, immutable, and follows the DNS label standard<br />as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-),<br />start and end with an alphanumeric character, and be no longer than 63 characters<br />[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63 <br />Required: \{\} <br /> |
343+
| `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a reference to a ServiceAccount used to perform all interactions<br />with the cluster that are required to manage the extension.<br /><opcon:standard:description><br />The ServiceAccount must be configured with the necessary permissions to perform these interactions.<br />serviceAccount is required.<br /></opcon:standard:description><br /><opcon:experimental:description><br />If serviceAccount is specified, OLM will authenticate as that service account.<br />Otherwise, operator-controller will authenticate as:<br /> - User: "olmv1:clusterextensions:<clusterExtensionName>"<br /> - Group: "olmv1:clusterextensions"<br />The authenticated user must be configured with the necessary permissions to perform these interactions.<br /></opcon:experimental:description><br /><opcon:standard:validation:Required><br /><opcon:experimental:validation:Optional> | | |
344344
| `source` _[SourceConfig](#sourceconfig)_ | source is a required field which selects the installation source of content<br />for this ClusterExtension. Selection is performed by setting the sourceType.<br />Catalog is currently the only implemented sourceType, and setting the<br />sourcetype to "Catalog" requires the catalog field to also be defined.<br />Below is a minimal example of a source definition (in yaml):<br />source:<br /> sourceType: Catalog<br /> catalog:<br /> packageName: example-package | | Required: \{\} <br /> |
345345
| `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is an optional field used to configure the installation options<br />for the ClusterExtension such as the pre-flight check configuration. | | |
346346
| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is an optional field used to specify bundle specific configuration<br />used to configure the bundle. Configuration is bundle specific and a bundle may provide<br />a configuration schema. When not specified, the default configuration of the resolved bundle will be used.<br />config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide<br />a configuration schema the final manifests will be derived on a best-effort basis. More information on how<br />to configure the bundle should be found in its end-user documentation.<br /><opcon:experimental> | | |
@@ -439,7 +439,7 @@ _Appears in:_
439439

440440

441441

442-
ServiceAccountReference identifies the serviceAccount name used for managing a ClusterExtension.
442+
ServiceAccountReference identifies the serviceAccount name used to manage a ClusterExtension.
443443

444444

445445

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
## Synthetic User Permissions
2+
3+
!!! note
4+
This feature is still in *alpha* the `SyntheticPermissions` feature-gate must be enabled to make use of it.
5+
See the instructions below on how to enable it.
6+
7+
Synthetic user permissions enables fine-grained configuration of ClusterExtension management client RBAC permissions.
8+
User can not only configure RBAC permissions governing the management across all ClusterExtensions, but also on a
9+
case-by-case basis.
10+
11+
### Run OLM v1 with Experimental Features Enabled
12+
13+
```terminal title=Enable Experimental Features in a New Kind Cluster
14+
make run-experimental
15+
```
16+
17+
```terminal title=Wait for rollout to complete
18+
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager
19+
```
20+
21+
### How does it work?
22+
23+
When managing a ClusterExtension that does not specify a service account, OLM will assume the identity of user
24+
"olm:clusterextensions:<clusterextension-name>" and group "olm:clusterextensions" limiting Kubernetes API access scope
25+
to those defined for this user and group. These users and group do not exist beyond being defined in
26+
Cluster/RoleBinding(s) and can only be impersonated by clients with `impersonate` verb permissions on the `users` and
27+
`groups` resources.
28+
29+
### Demo
30+
31+
[![asciicast](https://asciinema.org/a/Jbtt8nkV8Dm7vriHxq7sxiVvi.svg)](https://asciinema.org/a/Jbtt8nkV8Dm7vriHxq7sxiVvi)
32+
33+
#### Examples:
34+
35+
##### ClusterExtension management as cluster-admin
36+
37+
To enable ClusterExtensions management as cluster-admin, bind the `cluster-admin` cluster role to the `olm:clusterextensions`
38+
group:
39+
40+
```
41+
apiVersion: rbac.authorization.k8s.io/v1
42+
kind: ClusterRoleBinding
43+
metadata:
44+
name: clusterextensions-group-admin-binding
45+
roleRef:
46+
apiGroup: rbac.authorization.k8s.io
47+
kind: ClusterRole
48+
name: cluster-admin
49+
subjects:
50+
- kind: Group
51+
name: "olm:clusterextensions"
52+
```
53+
54+
##### Scoped olm:clusterextensions group + Added perms on specific extensions
55+
56+
Give ClusterExtension management group broad permissions to manage ClusterExtensions denying potentially dangerous
57+
permissions such as being able to read cluster wide secrets:
58+
59+
```
60+
apiVersion: rbac.authorization.k8s.io/v1
61+
kind: ClusterRole
62+
metadata:
63+
name: clusterextension-installer
64+
rules:
65+
- apiGroups: [ olm.operatorframework.io ]
66+
resources: [ clusterextensions/finalizers ]
67+
verbs: [ update ]
68+
- apiGroups: [ apiextensions.k8s.io ]
69+
resources: [ customresourcedefinitions ]
70+
verbs: [ create, list, watch, get, update, patch, delete ]
71+
- apiGroups: [ rbac.authorization.k8s.io ]
72+
resources: [ clusterroles, roles, clusterrolebindings, rolebindings ]
73+
verbs: [ create, list, watch, get, update, patch, delete ]
74+
- apiGroups: [""]
75+
resources: [configmaps, endpoints, events, pods, pod/logs, serviceaccounts, services, services/finalizers, namespaces, persistentvolumeclaims]
76+
verbs: ['*']
77+
- apiGroups: [apps]
78+
resources: [ '*' ]
79+
verbs: ['*']
80+
- apiGroups: [ batch ]
81+
resources: [ '*' ]
82+
verbs: [ '*' ]
83+
- apiGroups: [ networking.k8s.io ]
84+
resources: [ '*' ]
85+
verbs: [ '*' ]
86+
- apiGroups: [authentication.k8s.io]
87+
resources: [tokenreviews, subjectaccessreviews]
88+
verbs: [create]
89+
```
90+
91+
```
92+
apiVersion: rbac.authorization.k8s.io/v1
93+
kind: ClusterRoleBinding
94+
metadata:
95+
name: clusterextension-installer-binding
96+
roleRef:
97+
apiGroup: rbac.authorization.k8s.io
98+
kind: ClusterRole
99+
name: clusterextension-installer
100+
subjects:
101+
- kind: Group
102+
name: "olm:clusterextensions"
103+
```
104+
105+
Give a specific ClusterExtension secrets access, maybe even on specific namespaces:
106+
107+
```
108+
apiVersion: rbac.authorization.k8s.io/v1
109+
kind: ClusterRole
110+
metadata:
111+
name: clusterextension-privileged
112+
rules:
113+
- apiGroups: [""]
114+
resources: [secrets]
115+
verbs: ['*']
116+
```
117+
118+
```
119+
apiVersion: rbac.authorization.k8s.io/v1
120+
kind: RoleBinding
121+
metadata:
122+
name: clusterextension-privileged-binding
123+
namespace: <some namespace>
124+
roleRef:
125+
apiGroup: rbac.authorization.k8s.io
126+
kind: ClusterRole
127+
name: clusterextension-privileged
128+
subjects:
129+
- kind: User
130+
name: "olm:clusterextensions:argocd-operator"
131+
```
132+
133+
Note: In this example the ClusterExtension user (or group) will still need to be updated to be able to manage
134+
the CRs coming from the argocd operator. Some look ahead and RBAC permission wrangling will still be required.

0 commit comments

Comments
 (0)