Skip to content

Commit

Permalink
Switch to using the pod service account by default (#28)
Browse files Browse the repository at this point in the history
Motivation
------------
Since K8S 1.24, service accounts don't automatically generate `Secret` resources that contain a token that never expires. While it is possible to make such a token manually, it is more difficult to find the token because the `ServiceAccount` no longer gets a reference to the `Secret`. Additionally, this approach is less secure because the token doesn't expire and is not rotated.

Modifications
---------------
By default, assume that the service account for the pod has the access rights to `Endpoints` required by Shawarma. This eliminates the need to handle tokens altogether. This also eliminates the need to grant RBAC rights to the webhook as well. The user may still opt-in to the old behavior using the `SHAWARMA_SERVICE_ACCT_NAME` environment variable or `shawarma-service-acct-name` command line switch.

**BREAKING CHANGE**: The consumer must change the command line switch or environment variables used on the webhook to retain the default behavior *OR* update their service accounts to grant the `shawarma` role. This may be applied to the `default` service account in most cases, but any pods using a different service account will require the rights to be granted to that service account.
  • Loading branch information
brantburnett authored Jul 26, 2023
1 parent 81d377c commit 9b82654
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 71 deletions.
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ VOLUME [ "/tmp", "/etc/shawarma-webhook/certs" ]
ENV CERT_FILE=/etc/shawarma-webhook/certs/tls.crt \
KEY_FILE=/etc/shawarma-webhook/certs/tls.key \
WEBHOOK_PORT=8443 \
SHAWARMA_IMAGE=centeredge/shawarma:1.1.0 \
SHAWARMA_SERVICE_ACCT_NAME=shawarma \
SHAWARMA_IMAGE=centeredge/shawarma:2.0.0-beta001 \
LOG_LEVEL=warn

USER 65532:65532
Expand Down
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ your cluster to manage TLS between the API server and the webhook.

## RBAC Rights

If using `SHAWARMA_SERVICE_ACCT_NAME` (the default), the webhook needs the following RBAC rights bound to
the webhook's service account.
### Legacy Approach

If using `SHAWARMA_SERVICE_ACCT_NAME`, the webhook needs the following RBAC rights bound to the webhook's service account.

```yaml
apiVersion: rbac.authorization.k8s.io/v1
Expand All @@ -28,6 +29,40 @@ rules:
verbs: ["get", "watch", "list"]
```
Additionally, the service referenced by `SHAWARMA_SERVICE_ACCT_NAME` must have a legacy `Secret` linked to it.

### Modern Approach

The modern approach is to grant rights to the `serviceAccountName` used by the pod. This is more secure and provides token rotation, etc.
The rights may be granted to the `default` service account for a namespace, if desired.

```yaml
# Create the role that has the required rights for the Shawarma sidecar
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: shawarma
namespace: default
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "watch", "list"]
---
# Grant these rights to the default service account for a namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: shawarma-default
namespace: default
subjects:
- kind: ServiceAccount
name: default
roleRef:
kind: Role
name: shawarma
apiGroup: rbac.authorization.k8s.io
```

## Environment Variables

The following environment variables may be used to customize behaviors of the webhook.
Expand All @@ -39,7 +74,7 @@ The following environment variables may be used to customize behaviors of the we
| CERT_FILE | /etc/shawarma-webhook/certs/tls.crt | Certificate file used for TLS by the admission webhook |
| KEY_FILE | /etc/shawarma-webhook/certs/tls.key | Key file used for TLS by the admission webhook |
| SWAWARMA_IMAGE | centeredge/shawarma:1.0.0 | Default Shawarma image |
| SHAWARMA_SERVICE_ACCT_NAME | shawarma | Name of the service account which should be used for sidecars |
| SHAWARMA_SERVICE_ACCT_NAME | | Name of the service account which should be used for sidecars (requires a legacy token secret linked to the service account) |
| SHAWARMA_SECRET_TOKEN_NAME | | Name of the secret containing the Kubernetes token for Shawarma, overrides SHAWARMA_SERVICE_ACCT_NAME |

## Annotations
Expand Down Expand Up @@ -70,3 +105,7 @@ allocations or other details of the sidecar.
| `SHAWARMA_TOKEN_NAME` | Must be in a volume `secretName`, replaced with the name of the secret containing the Shawarma token for K8S API access |

> For an example SIDECAR_CONFIG file, see [sidecar.yaml](./sidecar.yaml).

The example contains two different sidecar definitions `shawarma` and `shawarma-withtoken`. The default is `shawarma`, but `shawarma-withtoken`
is used if the `SHAWARMA_SERVICE_ACCT_NAME` OR `SHAWARMA_SECRET_TOKEN_NAME` environment variables (or equivalent command line arguments) are used
to provide legacy API authentication via a `Secret`.
22 changes: 13 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func main() {
app := cli.NewApp()
app.Name = "Shawarma Webhook"
app.Usage = "Kubernetes Mutating Admission Webhook to add the Shawarma sidecar when requested by annotations"
app.Copyright = "(c) 2019-2022 CenterEdge Software"
app.Copyright = "(c) 2019-2023 CenterEdge Software"
app.Version = version
app.HideHelpCommand = true

Expand Down Expand Up @@ -70,13 +70,13 @@ func main() {
&cli.StringFlag{
Name: "shawarma-image",
Usage: "Default Docker image",
Value: "centeredge/shawarma:1.1.0",
Value: "centeredge/shawarma:2.0.0-beta001",
EnvVars: []string{"SHAWARMA_IMAGE"},
},
&cli.StringFlag{
Name: "shawarma-service-acct-name",
Usage: "Name of the service account which should be used for sidecars",
Value: "shawarma",
Usage: "Name of the service account which should be used for sidecars (requires a legacy token secret linked to the service account)",
Value: "",
EnvVars: []string{"SHAWARMA_SERVICE_ACCT_NAME"},
},
&cli.StringFlag{
Expand All @@ -101,16 +101,20 @@ func main() {

log.SetLevel(level)

err = webhook.InitializeServiceAcctMonitor()
if err != nil {
log.Warn(err)
}

return nil
}

app.Action = func(c *cli.Context) error {
conf := readConfig(c)

if conf.shawarmaServiceAcctName != "" {
// If using a service account token, startup the monitor for service accounts
err := webhook.InitializeServiceAcctMonitor()
if err != nil {
log.Warn(err)
}
}

simpleServer := httpd.NewSimpleServer(conf.httpdConf)

webhook.Init()
Expand Down
6 changes: 3 additions & 3 deletions routes/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type SideCar struct {
Sidecar webhook.SideCar `json:"sidecar"`
}

func loadConfig(sideCarConfigFile string) (map[string]*webhook.SideCar, error) {
func loadConfig(sideCarConfigFile string) (map[string]webhook.SideCar, error) {
data, err := ioutil.ReadFile(sideCarConfigFile)
if err != nil {
return nil, err
Expand All @@ -35,9 +35,9 @@ func loadConfig(sideCarConfigFile string) (map[string]*webhook.SideCar, error) {
return nil, err
}

mapOfSideCar := make(map[string]*webhook.SideCar)
mapOfSideCar := make(map[string]webhook.SideCar)
for _, configuration := range cfg.Sidecars {
mapOfSideCar[configuration.Name] = &configuration.Sidecar
mapOfSideCar[configuration.Name] = configuration.Sidecar
}

return mapOfSideCar, nil
Expand Down
56 changes: 53 additions & 3 deletions sidecar.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
sidecars:
- name: shawarma
sidecar:
containers:
- name: shawarma
image: "|SHAWARMA_IMAGE|"
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
runAsNonRoot: true
env:
- name: LOG_LEVEL
valueFrom:
fieldRef:
fieldPath: metadata.annotations['shawarma.centeredge.io/log-level']
- name: SHAWARMA_SERVICE
# References service to monitor
valueFrom:
fieldRef:
fieldPath: metadata.annotations['shawarma.centeredge.io/service-name']
- name: SHAWARMA_SERVICE_LABELS
# References service to monitor
valueFrom:
fieldRef:
fieldPath: metadata.annotations['shawarma.centeredge.io/service-labels']
- name: SHAWARMA_URL
# Will POST state to this URL as pod is attached/detached from the service
valueFrom:
fieldRef:
fieldPath: metadata.annotations['shawarma.centeredge.io/state-url']
- name: SHAWARMA_LISTEN_PORT
# Will listen for HTTP GET of state on this port, localhost traffic only
valueFrom:
fieldRef:
fieldPath: metadata.annotations['shawarma.centeredge.io/listen-port']
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 25m
memory: 64Mi
- name: shawarma-withtoken
sidecar:
volumes:
- name: shawarma-token
Expand Down Expand Up @@ -54,9 +105,8 @@ sidecars:
fieldPath: metadata.namespace
resources:
requests:
cpu: 10m
cpu: 25m
memory: 64Mi
limits:
cpu: 10m
cpu: 25m
memory: 64Mi

43 changes: 2 additions & 41 deletions tests/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,6 @@ metadata:
labels:
shawarma-injection: enabled
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: shawarma
namespace: shawarma-test
secrets:
- name: shawarma-token
---
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: shawarma-token
namespace: shawarma-test
annotations:
kubernetes.io/service-account.name: shawarma
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
Expand All @@ -35,38 +18,16 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: shawarma
name: shawarma-default
namespace: shawarma-test
subjects:
- kind: ServiceAccount
name: shawarma
name: default
roleRef:
kind: Role
name: shawarma
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: shawarma-webhook
rules:
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: shawarma
subjects:
- kind: ServiceAccount
name: shawarma-webhook
namespace: kube-system
roleRef:
kind: ClusterRole
name: shawarma-webhook
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down
2 changes: 1 addition & 1 deletion tests/webhook-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ spec:
- name: LOG_LEVEL
value: debug
- name: SHAWARMA_IMAGE
value: centeredge/shawarma:1.1.0
value: centeredge/shawarma:2.0.0-beta001
ports:
- name: https
containerPort: 8443
Expand Down
25 changes: 16 additions & 9 deletions webhook/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
sideCarInjectionImageAnnotation = sideCarNameSpace + imageAnnotation
injectedValue = "injected"
sideCarName = "shawarma"
sideCarWithTokenName = "shawarma-withtoken"
)

// unversionedAdmissionReview is used to decode both v1 and v1beta1 AdmissionReview types.
Expand All @@ -62,7 +63,7 @@ type SideCar struct {

/*Mutator is the interface for mutating webhook*/
type Mutator struct {
SideCars map[string]*SideCar
SideCars map[string]SideCar
ShawarmaImage string
ShawarmaServiceAcctName string
ShawarmaSecretTokenName string
Expand Down Expand Up @@ -137,7 +138,7 @@ func mutate(req *v1.AdmissionRequest, mutator *Mutator) *v1.AdmissionResponse {
return errorResponse(req.UID, err)
}

if sideCarNames, ok := shouldMutate(systemNameSpaces, &pod.ObjectMeta, req.Namespace); ok {
if sideCarNames, ok := shouldMutate(systemNameSpaces, &pod.ObjectMeta, req.Namespace, mutator); ok {
annotations := map[string]string{sideCarInjectionStatusAnnotation: injectedValue}
patchBytes, err := createPatch(&pod, req.Namespace, sideCarNames, mutator, annotations)
if err != nil {
Expand Down Expand Up @@ -174,7 +175,7 @@ func unMarshall(req *v1.AdmissionRequest) (corev1.Pod, error) {
return pod, err
}

func shouldMutate(ignoredList []string, metadata *metav1.ObjectMeta, namespace string) ([]string, bool) {
func shouldMutate(ignoredList []string, metadata *metav1.ObjectMeta, namespace string, mutator *Mutator) ([]string, bool) {
podName := metadata.Name
if podName == "" {
podName = metadata.GenerateName
Expand All @@ -197,17 +198,23 @@ func shouldMutate(ignoredList []string, metadata *metav1.ObjectMeta, namespace s
return nil, false
}

selectedSideCarName := sideCarName
if mutator.ShawarmaSecretTokenName != "" || mutator.ShawarmaServiceAcctName != "" {
// We need to attach a token, use the alternate side car format
selectedSideCarName = sideCarWithTokenName
}

if serviceName, ok := annotations[sideCarInjectionAnnotation]; ok {
if len(serviceName) > 0 {
log.Infof("shawarma injection for %v/%v: service-name: %v", namespace, podName, serviceName)
return []string{sideCarName}, true
log.Infof("shawarma injection for %v/%v: service-name: %v sidecar: %v", namespace, podName, serviceName, selectedSideCarName)
return []string{selectedSideCarName}, true
}
}

if serviceLabels, ok := annotations[sideCarLabelInjectionAnnotation]; ok {
if len(serviceLabels) > 0 {
log.Infof("shawarma injection for %v/%v: service-labels: %v", namespace, podName, serviceLabels)
return []string{sideCarName}, true
log.Infof("shawarma injection for %v/%v: service-labels: %v sidecar: %v", namespace, podName, serviceLabels, selectedSideCarName)
return []string{selectedSideCarName}, true
}
}

Expand Down Expand Up @@ -235,7 +242,7 @@ func createPatch(pod *corev1.Pod, namespace string, sideCarNames []string, mutat

// Handle the secret name
secretName := mutator.ShawarmaSecretTokenName
if secretName == "" {
if secretName == "" && mutator.ShawarmaServiceAcctName != "" {
// Get the secret name from the service account
monitor, err := mutator.ServiceAcctMonitors.Get(namespace, mutator.ShawarmaServiceAcctName, time.Second*1)
if err != nil {
Expand All @@ -252,7 +259,7 @@ func createPatch(pod *corev1.Pod, namespace string, sideCarNames []string, mutat

for _, name := range sideCarNames {
if sideCar, ok := mutator.SideCars[name]; ok {
sideCarCopy := *sideCar
sideCarCopy := sideCar

sideCarCopy.Containers = make([]corev1.Container, len(sideCar.Containers))
sideCarCopy.Volumes = make([]corev1.Volume, len(sideCar.Volumes))
Expand Down

0 comments on commit 9b82654

Please sign in to comment.