Skip to content

Commit 6cd629e

Browse files
joelanfordPer Goncalves da Silva
and
Per Goncalves da Silva
authored
✨ auth: use synthetic user/group when service account is not defined (#1816)
* auth: use synthetic user/group when service account is not defined Signed-off-by: Joe Lanford <[email protected]> * Revert API changes Signed-off-by: Per Goncalves da Silva <[email protected]> * Add featuregate Signed-off-by: Per Goncalves da Silva <[email protected]> * Add unit tests Signed-off-by: Per Goncalves da Silva <[email protected]> * Update syntheric user format Signed-off-by: Per Goncalves da Silva <[email protected]> * Add featuregate kustomize overlay Signed-off-by: Per Goncalves da Silva <[email protected]> * Add demo resources Signed-off-by: Per Goncalves da Silva <[email protected]> * Refactor synthetic auth exported functions Signed-off-by: Per Goncalves da Silva <[email protected]> * Update demo Signed-off-by: Per Goncalves da Silva <[email protected]> * Add some docs Signed-off-by: Per Goncalves da Silva <[email protected]> * Clean up demo resources Signed-off-by: Per Goncalves da Silva <[email protected]> * Address reviewer comments Signed-off-by: Per Goncalves da Silva <[email protected]> * Move FG check to main Signed-off-by: Per Goncalves da Silva <[email protected]> --------- Signed-off-by: Joe Lanford <[email protected]> Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent b4763ae commit 6cd629e

File tree

13 files changed

+508
-5
lines changed

13 files changed

+508
-5
lines changed

cmd/operator-controller/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ func run() error {
305305
}
306306
tokenGetter := authentication.NewTokenGetter(coreClient, authentication.WithExpirationDuration(1*time.Hour))
307307
clientRestConfigMapper := action.ServiceAccountRestConfigMapper(tokenGetter)
308+
if features.OperatorControllerFeatureGate.Enabled(features.SyntheticPermissions) {
309+
clientRestConfigMapper = action.SyntheticUserRestConfigMapper(clientRestConfigMapper)
310+
}
308311

309312
cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(),
310313
helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# kustomization file for secure OLMv1
2+
# DO NOT ADD A NAMESPACE HERE
3+
apiVersion: kustomize.config.k8s.io/v1beta1
4+
kind: Kustomization
5+
resources:
6+
- ../../../base/operator-controller
7+
- ../../../base/common
8+
components:
9+
- ../../../components/tls/operator-controller
10+
11+
patches:
12+
- target:
13+
kind: Deployment
14+
name: operator-controller-controller-manager
15+
path: patches/enable-featuregate.yaml
16+
- target:
17+
kind: ClusterRole
18+
name: operator-controller-manager-role
19+
path: patches/impersonate-perms.yaml
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# enable synthetic-user feature gate
2+
- op: add
3+
path: /spec/template/spec/containers/0/args/-
4+
value: "--feature-gates=SyntheticPermissions=true"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# enable synthetic-user feature gate
2+
- op: add
3+
path: /rules/-
4+
value:
5+
apiGroups:
6+
- ""
7+
resources:
8+
- groups
9+
- users
10+
verbs:
11+
- impersonate
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
### Update OLM to enable Feature
12+
13+
```terminal title=Enable SyntheticPermissions feature
14+
kubectl kustomize config/overlays/featuregate/synthetic-user-permissions | kubectl apply -f -
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, OLM will assume the identity of user "olm:clusterextensions:<clusterextension-name>"
24+
and group "olm:clusterextensions" limiting Kubernetes API access scope to those defined for this user and group. These
25+
users and group do not exist beyond being defined in Cluster/RoleBinding(s) and can only be impersonated by clients with
26+
`impersonate` verb permissions on the `users` and `groups` resources.
27+
28+
### Demo
29+
30+
[![asciicast](https://asciinema.org/a/Jbtt8nkV8Dm7vriHxq7sxiVvi.svg)](https://asciinema.org/a/Jbtt8nkV8Dm7vriHxq7sxiVvi)
31+
32+
#### Examples:
33+
34+
##### ClusterExtension management as cluster-admin
35+
36+
To enable ClusterExtensions management as cluster-admin, bind the `cluster-admin` cluster role to the `olm:clusterextensions`
37+
group:
38+
39+
```
40+
apiVersion: rbac.authorization.k8s.io/v1
41+
kind: ClusterRoleBinding
42+
metadata:
43+
name: clusterextensions-group-admin-binding
44+
roleRef:
45+
apiGroup: rbac.authorization.k8s.io
46+
kind: ClusterRole
47+
name: cluster-admin
48+
subjects:
49+
- kind: Group
50+
name: "olm:clusterextensions"
51+
```
52+
53+
##### Scoped olm:clusterextension group + Added perms on specific extensions
54+
55+
Give ClusterExtension management group broad permissions to manage ClusterExtensions denying potentially dangerous
56+
permissions such as being able to read cluster wide secrets:
57+
58+
```
59+
apiVersion: rbac.authorization.k8s.io/v1
60+
kind: ClusterRole
61+
metadata:
62+
name: clusterextension-installer
63+
rules:
64+
- apiGroups: [ olm.operatorframework.io ]
65+
resources: [ clusterextensions/finalizers ]
66+
verbs: [ update ]
67+
- apiGroups: [ apiextensions.k8s.io ]
68+
resources: [ customresourcedefinitions ]
69+
verbs: [ create, list, watch, get, update, patch, delete ]
70+
- apiGroups: [ rbac.authorization.k8s.io ]
71+
resources: [ clusterroles, roles, clusterrolebindings, rolebindings ]
72+
verbs: [ create, list, watch, get, update, patch, delete ]
73+
- apiGroups: [""]
74+
resources: [configmaps, endpoints, events, pods, pod/logs, serviceaccounts, services, services/finalizers, namespaces, persistentvolumeclaims]
75+
verbs: ['*']
76+
- apiGroups: [apps]
77+
resources: [ '*' ]
78+
verbs: ['*']
79+
- apiGroups: [ batch ]
80+
resources: [ '*' ]
81+
verbs: [ '*' ]
82+
- apiGroups: [ networking.k8s.io ]
83+
resources: [ '*' ]
84+
verbs: [ '*' ]
85+
- apiGroups: [authentication.k8s.io]
86+
resources: [tokenreviews, subjectaccessreviews]
87+
verbs: [create]
88+
```
89+
90+
```
91+
apiVersion: rbac.authorization.k8s.io/v1
92+
kind: ClusterRoleBinding
93+
metadata:
94+
name: clusterextension-installer-binding
95+
roleRef:
96+
apiGroup: rbac.authorization.k8s.io
97+
kind: ClusterRole
98+
name: clusterextension-installer
99+
subjects:
100+
- kind: Group
101+
name: "olm:clusterextensions"
102+
```
103+
104+
Give a specific ClusterExtension secrets access, maybe even on specific namespaces:
105+
106+
```
107+
apiVersion: rbac.authorization.k8s.io/v1
108+
kind: ClusterRole
109+
metadata:
110+
name: clusterextension-privileged
111+
rules:
112+
- apiGroups: [""]
113+
resources: [secrets]
114+
verbs: ['*']
115+
```
116+
117+
```
118+
apiVersion: rbac.authorization.k8s.io/v1
119+
kind: RoleBinding
120+
metadata:
121+
name: clusterextension-privileged-binding
122+
namespace: <some namespace>
123+
roleRef:
124+
apiGroup: rbac.authorization.k8s.io
125+
kind: ClusterRole
126+
name: clusterextension-privileged
127+
subjects:
128+
- kind: User
129+
name: "olm:clusterextensions:argocd-operator"
130+
```
131+
132+
Note: In this example the ClusterExtension user (or group) will still need to be updated to be able to manage
133+
the CRs coming from the argocd operator. Some look ahead and RBAC permission wrangling will still be required.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: olm.operatorframework.io/v1
2+
kind: ClusterExtension
3+
metadata:
4+
name: argocd-operator
5+
spec:
6+
namespace: argocd-system
7+
serviceAccount:
8+
name: "olm.synthetic-user"
9+
source:
10+
sourceType: Catalog
11+
catalog:
12+
packageName: argocd-operator
13+
version: 0.6.0
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: ClusterRoleBinding
3+
metadata:
4+
name: clusterextensions-group-admin-binding
5+
roleRef:
6+
apiGroup: rbac.authorization.k8s.io
7+
kind: ClusterRole
8+
name: cluster-admin
9+
subjects:
10+
- kind: Group
11+
name: "olm:clusterextensions"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
3+
#
4+
# Welcome to the SingleNamespace install mode demo
5+
#
6+
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
7+
8+
# enable 'SyntheticPermissions' feature
9+
kubectl kustomize config/overlays/featuregate/synthetic-user-permissions | kubectl apply -f -
10+
11+
# wait for operator-controller to become available
12+
kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager
13+
14+
# create install namespace
15+
kubectl create ns argocd-system
16+
17+
# give cluster extension group cluster admin privileges - all cluster extensions installer users will be cluster admin
18+
bat --style=plain ${DEMO_RESOURCE_DIR}/synthetic-user-perms/cegroup-admin-binding.yaml
19+
20+
# apply cluster role binding
21+
kubectl apply -f ${DEMO_RESOURCE_DIR}/synthetic-user-perms/cegroup-admin-binding.yaml
22+
23+
# install cluster extension - for now .spec.serviceAccount = "olm.synthetic-user"
24+
bat --style=plain ${DEMO_RESOURCE_DIR}/synthetic-user-perms/argocd-clusterextension.yaml
25+
26+
# apply cluster extension
27+
kubectl apply -f ${DEMO_RESOURCE_DIR}/synthetic-user-perms/argocd-clusterextension.yaml
28+
29+
# wait for cluster extension installation to succeed
30+
kubectl wait --for=condition=Installed clusterextension/argocd-operator --timeout="60s"

internal/operator-controller/action/restconfig.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,73 @@ package action
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67

78
"k8s.io/apimachinery/pkg/types"
89
"k8s.io/client-go/rest"
10+
"k8s.io/client-go/transport"
911
"sigs.k8s.io/controller-runtime/pkg/client"
1012

1113
ocv1 "github.com/operator-framework/operator-controller/api/v1"
1214
"github.com/operator-framework/operator-controller/internal/operator-controller/authentication"
1315
)
1416

17+
const syntheticServiceAccountName = "olm.synthetic-user"
18+
19+
// SyntheticUserRestConfigMapper returns an AuthConfigMapper that that impersonates synthetic users and groups for Object o.
20+
// o is expected to be a ClusterExtension. If the service account defined in o is different from 'olm.synthetic-user', the
21+
// defaultAuthMapper will be used
22+
func SyntheticUserRestConfigMapper(defaultAuthMapper func(ctx context.Context, o client.Object, c *rest.Config) (*rest.Config, error)) func(ctx context.Context, o client.Object, c *rest.Config) (*rest.Config, error) {
23+
return func(ctx context.Context, o client.Object, c *rest.Config) (*rest.Config, error) {
24+
cExt, err := validate(o, c)
25+
if err != nil {
26+
return nil, err
27+
}
28+
if cExt.Spec.ServiceAccount.Name != syntheticServiceAccountName {
29+
return defaultAuthMapper(ctx, cExt, c)
30+
}
31+
cc := rest.CopyConfig(c)
32+
cc.Wrap(func(rt http.RoundTripper) http.RoundTripper {
33+
return transport.NewImpersonatingRoundTripper(authentication.SyntheticImpersonationConfig(*cExt), rt)
34+
})
35+
return cc, nil
36+
}
37+
}
38+
39+
// ServiceAccountRestConfigMapper returns an AuthConfigMapper scoped to the service account defined in o, which is expected to
40+
// be a ClusterExtension
1541
func ServiceAccountRestConfigMapper(tokenGetter *authentication.TokenGetter) func(ctx context.Context, o client.Object, c *rest.Config) (*rest.Config, error) {
1642
return func(ctx context.Context, o client.Object, c *rest.Config) (*rest.Config, error) {
17-
cExt := o.(*ocv1.ClusterExtension)
18-
saKey := types.NamespacedName{
19-
Name: cExt.Spec.ServiceAccount.Name,
20-
Namespace: cExt.Spec.Namespace,
43+
cExt, err := validate(o, c)
44+
if err != nil {
45+
return nil, err
2146
}
2247
saConfig := rest.AnonymousClientConfig(c)
2348
saConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper {
2449
return &authentication.TokenInjectingRoundTripper{
2550
Tripper: rt,
2651
TokenGetter: tokenGetter,
27-
Key: saKey,
52+
Key: types.NamespacedName{
53+
Name: cExt.Spec.ServiceAccount.Name,
54+
Namespace: cExt.Spec.Namespace,
55+
},
2856
}
2957
})
3058
return saConfig, nil
3159
}
3260
}
61+
62+
func validate(o client.Object, c *rest.Config) (*ocv1.ClusterExtension, error) {
63+
if c == nil {
64+
return nil, fmt.Errorf("rest config is nil")
65+
}
66+
if o == nil {
67+
return nil, fmt.Errorf("object is nil")
68+
}
69+
cExt, ok := o.(*ocv1.ClusterExtension)
70+
if !ok {
71+
return nil, fmt.Errorf("object is not a ClusterExtension")
72+
}
73+
return cExt, nil
74+
}

0 commit comments

Comments
 (0)