Published on 00/00/0000
Last updated on 00/00/0000
Published on 00/00/0000
Last updated on 00/00/0000
Share
Share
13 min read
Share
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
Because identity is the basis for authorization and because the service account is the basis for identity, it's important to set up an environment correctly, using proper policies. In Kubernetes, RBAC rules can and should be used to specify such policies and permission settings.
For starters, users and service accounts should be restricted to a set of namespaces. Of course, this is the bare minimum setting, and it is highly recommended that you use a more fine-grained RBAC setup. On the other hand, this does not prevents issues that might arise from misconfiguration or due to a malicious actor trying to run a workload in the name of a specific service account.
If you need a hand with this, you can create a cluster with our free version of Banzai Cloud's Pipeline platform.
KUBECONFIG
at your cluster.~ ❯ helm repo add stable https://kubernetes-charts.storage.googleapis.com/
"stable" has been added to your repositories
~ ❯ kubectl create namespace opa
namespace/opa created
~ ❯ helm upgrade --install opa stable/opa --namespace opa \
--values https://raw.githubusercontent.com/banzaicloud/opa-samples/master/helm-values.yaml
We are going to use the same application that is used for generic Backyards demonstrations, called Allspark.
~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/data-service-deploy.yaml
namespace/data created
serviceaccount/data created
deployment.apps/data created
service/data created
Configure an Istio authorization policy so that only the analytics service is allowed to reach the data service's /api/v1/data
endpoint.
~ ❯ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: data
namespace: data
spec:
selector:
matchLabels:
app: data
rules:
- from:
- source:
principals: ["cluster.local/ns/analytics/sa/analytics"]
to:
- operation:
methods: ["GET"]
paths: ["/api/v1/data"]
EOF
Now let's deploy the analytics service, which runs with the analytics service account and checks whether it's possible to connect to the data service.
~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/analytics-service-deploy.yaml
namespace/analytics created
serviceaccount/analytics created
deployment.apps/analytics created
service/analytics created
~ ❯ kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: analytics
labels:
app: test-pod
spec:
containers:
- name: app
image: curlimages/curl:7.72.0
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 3000; done;" ]
EOF
pod/test-pod created
~ ❯ kubectl exec -ti test-pod -c app -- curl http://analytics:8080
analytics response
By checking the logs, we can determine that the analytics service was able to reach the data service.
~ ❯ kubectl logs -l app=analytics -c service | tail -2
time="2020-09-09T21:48:27Z" level=info msg="outgoing request" correlationID=70fad725-7791-4070-b655-1f80a85730f1 server=http url="http://data.data:8080/api/v1/data"
time="2020-09-09T21:48:27Z" level=info msg="response to outgoing request" correlationID=70fad725-7791-4070-b655-1f80a85730f1 responseCode=200 server=http url="http://data.data:8080/api/v1/data"
However, the test pod is not able to reach the data service directly, since the Istio authorization policy forbids that.
~ ❯ kubectl exec -ti test-pod -c app -- curl http://data.data:8080/api/v1/data
RBAC: access denied
We've anticipated everything that's happened so far. Now let's see what happens if we're trying to use another test pod, but this time we start it with the analytics service account.
~ ❯ kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-with-analytics
namespace: analytics
labels:
app: test-pod
spec:
serviceAccount: analytics
containers:
- name: app
image: curlimages/curl:7.72.0
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 3000; done;" ]
EOF
pod/test-pod-with-analytics created
Check the communication from the test-pod-with-analytics pod.
~ ❯ kubectl exec -ti test-pod-with-analytics -c app -- curl http://data.data:8080/api/v1/data
data service response
As expected, this shows that the Istio authorization policy allows the analytics service account to connect to the data service, regardless of the actual workload using that service account.
~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
labels:
openpolicyagent.org/policy: rego
name: opa-main
namespace: opa
data:
main: |
package system
import data.kubernetes.admission
main = {
"apiVersion": "admission.k8s.io/v1beta1",
"kind": "AdmissionReview",
"response": response,
}
default uid = ""
uid = input.request.uid
response = {
"allowed": false,
"uid": uid,
"status": {
"reason": reason,
},
} {
reason = concat(", ", admission.deny)
reason != ""
}
else = {"allowed": true, "uid": uid}
EOF
Besides the main configuration, we can add policies as configmaps and label them for OPA to discover. After a configmap is applied, the OPA policy manager annotates it, using the key openpolicyagent.org/policy-status
, which contains the actual policy status. Any errors in the policy are reported using this annotation.
The following OPA policy describes a scenario in which only the banzaicloud/allspark:0.1.2
and the banzaicloud/istio-proxyv2:1.7.0-bzc
images are allowed to run with the analytics service account. The first is the actual workload, and the second is the Istio proxy container image.
~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
labels:
openpolicyagent.org/policy: rego
name: opa-pod-allowlist
namespace: opa
data:
main: |
package kubernetes.admission
allowlist = [
{
"serviceAccount": "analytics",
"images": {"banzaicloud/allspark:0.1.2", "banzaicloud/istio-proxyv2:1.7.0-bzc"},
},
]
deny[msg] {
input.request.kind.kind == "Pod"
input.request.operation == "CREATE"
serviceAccount := input.request.object.spec.serviceAccountName
# check whether the service account is restricted
allowlist[a].serviceAccount == serviceAccount
image := input.request.object.spec.containers[_].image
# check whether the pod images allowed to run with the specified service account
not imageWithServiceAccountAllowed(serviceAccount, image)
msg := sprintf("pod with serviceAccount %q, image %q is not allowed", [serviceAccount, image])
}
imageWithServiceAccountAllowed(serviceAccount, image) {
allowlist[a].serviceAccount == serviceAccount
allowlist[a].images[image]
}
EOF
configmap/opa-pod-allowlist created
~ ❯ kubectl -n opa get cm opa-pod-allowlist -o jsonpath='{.metadata.annotations.openpolicyagent\.org/policy-status}'
{"status":"ok"}
After applying the OPA policy, we should try to re-create the test pod with the analytics service account and watch it fail miserably.
~ ❯ kubectl delete pods test-pod-analytics --grace-period=0
pod "test-pod-analytics" deleted
~ ❯ kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test-pod-with-analytics
namespace: analytics
labels:
app: test-pod
spec:
serviceAccount: analytics
containers:
- name: app
image: banzaicloud/allspark:0.1.2
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 3000; done;" ]
EOF
Error from server (pod with serviceAccount "analytics", image "curlimages/curl:7.72.0" is not allowed): error when creating "STDIN": admission webhook "webhook.openpolicyagent.org" denied the request: pod with serviceAccount "analytics", image "curlimages/curl:7.72.0" is not allowed
The OPA validating admission controller has to be installed on the new cluster as well. To do this, repeat step 4. from the Setup section on the new cluster and apply the opa-main
configmap.
~ ❯ backyards istio cluster attach ~/Download/waynz0r-0910-01.yaml
✓ creating service account and rbac permissions
...
✓ attaching cluster started successfully name=waynz0r-0910-01
~ ❯ backyards istio cluster status
Name Type Status Gateway Address Istio Control Plane Message
Clusters in the mesh
Name Type Status Gateway Address Istio Control Plane Message
waynz0r-0908-01 Host Available [18.195.59.67 52.57.74.102] -
waynz0r-0910-01 Peer Available [35.204.169.33] cp-v17x.istio-system
The `waynz0r-0908-01` cluster is running on AWS in the `eu-central-1` region, the `waynz0r-0910-01` cluster is a GKE cluster in the `europe-west4` region.
The following OPA policy was extended to support restrictions based on the `nodeSelector` property of the pods in question. Let's apply it to both clusters!
```bash
~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
labels:
openpolicyagent.org/policy: rego
name: opa-pod-allowlist
namespace: opa
data:
main: |
package kubernetes.admission
allowlist = [
{
"serviceAccount": "analytics",
"images": {"banzaicloud/allspark:0.1.2", "banzaicloud/istio-proxyv2:1.7.0-bzc"},
"nodeSelector": [{"failure-domain.beta.kubernetes.io/region": "eu-central-1"}],
},
]
deny[msg] {
input.request.kind.kind == "Pod"
input.request.operation == "CREATE"
serviceAccount := input.request.object.spec.serviceAccountName
# check whether the service account is restricted
allowlist[a].serviceAccount == serviceAccount
image := input.request.object.spec.containers[_].image
# check whether the pod images allowed to run with the specified service account
not imageWithServiceAccountAllowed(serviceAccount, image)
msg := sprintf("pod with serviceAccount %q, image %q is not allowed", [serviceAccount, image])
}
imageWithServiceAccountAllowed(serviceAccount, image) {
allowlist[a].serviceAccount == serviceAccount
allowlist[a].images[image]
}
deny[msg] {
input.request.kind.kind == "Pod"
input.request.operation == "CREATE"
serviceAccount := input.request.object.spec.serviceAccountName
# check whether the service account is restricted
allowlist[a].serviceAccount == serviceAccount
# check whether pod location is restricted
count(allowlist[a].nodeSelector[ns]) > 0
image := input.request.object.spec.containers[_].image
nodeSelector := object.get(input.request.object.spec, "nodeSelector", [])
# check whether pod location is allowed
not podAtLocationAllowed(serviceAccount, nodeSelector)
msg := sprintf("pod with serviceAccount %q, image %q is not allowed at the specified location", [serviceAccount, image])
}
podAtLocationAllowed(serviceAccount, nodeSelector) {
allowlist[a].serviceAccount == serviceAccount
# requires that at least one nodeSelector combination matches this image and serviceAccount combination
selcount := count(allowlist[a].nodeSelector[ns])
count({k | allowlist[a].nodeSelector[s][k] == nodeSelector[k]}) == selcount
}
EOF
configmap/opa-pod-allowlist created
Now let's see what happens when we try to deploy the analytics service to the peer cluster.
~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/analytics-service-deploy.yaml
namespace/analytics created
serviceaccount/analytics created
deployment.apps/analytics created
service/analytics created
This looks fine at first glance, but the pod hasn't actually been created, since the OPA policy prevented that from happening.
~ ❯ kubectl get event
LAST SEEN TYPE REASON OBJECT MESSAGE
2m24s Warning FailedCreate replicaset/analytics-6cb4bfc97f Error creating: admission webhook "webhook.openpolicyagent.org" denied the request: pod with serviceAccount "analytics", image "banzaicloud/allspark:0.1.2" is not allowed at the specified location, pod with serviceAccount "analytics", image "banzaicloud/istio-proxyv2:1.7.0-bzc" is not allowed at the specified location
It must be noted that it's important to protect these OPA policies, so proper Kubernetes RBAC rules need to be applied to prevent unwanted access to the opa
namespace and the validating webhook configuration resource!
Get emerging insights on emerging technology straight to your inbox.
Outshift is leading the way in building an open, interoperable, agent-first, quantum-safe infrastructure for the future of artificial intelligence.
* No email required
The Shift is Outshift’s exclusive newsletter.
Get the latest news and updates on generative AI, quantum computing, and other groundbreaking innovations shaping the future of technology.