- Part 1: Deploying K3s, network and host machine security configuration
- Part 2: K3s Securing the cluster
- Part 3: Creating a security responsive K3s cluster
This is part 2 in a three part blog series on deploying k3s, a certified Kubernetes distribution from SUSE Rancher, in a secure and available fashion. In the previous blog we secured the network, host operating system and deployed k3s. Note, a fullying working Ansible project, https://github.com/digitalis-io/k3s-on-prem-production, has been made available to deploy and secure k3s for you.
If you would like to know more about how to implement modern data and cloud technologies, such as Kubernetes, into to your business, we at Digitalis do it all: from cloud migration to fully managed services, we can help you modernize your operations, data, and applications. We provide consulting and managed services on Kubernetes, cloud, data, and DevOps for any business type. Contact us today for more information or learn more about each of our services here.
Introduction
So we have a running K3s cluster, are we done yet (see part 1)? Not at all!
We have secured the underlying machines and we have secured the network using strong segregation, but how about the cluster itself? There is still alot to think about and handle, so let’s take a look at some dangerous patterns.
Pod escaping
Let’s suppose we want to give someone the edit cluster role permission so that they can deploy pods, but obviously not an administrator account. We expect the account to be just able to stay in its own namespace and not harm the rest of the cluster, right?
Let’s create the user:
~ $ kubectl create namespace unprivileged-user
~ $ kubectl create serviceaccount -n unprivileged-user fake-user
~ $ kubectl create rolebinding -n unprivileged-user fake-editor --clusterrole=edit \
--serviceaccount=unprivileged-user:fake-user
Obviously the user cannot do much outside of his own namespace
~ $ kubectl-user get pods -A
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:unprivileged-user:fake-user" cannot list resource "pods" in API group "" at the cluster scope
But let’s say we want to deploy a privileged POD? Are we allowed to? Let’s deploy this
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: privileged-deploy
name: privileged-deploy
spec:
replicas: 1
selector:
matchLabels:
app: privileged-deploy
template:
metadata:
labels:
app: privileged-deploy
spec:
containers:
- image: alpine
name: alpine
stdin: true
tty: true
securityContext:
privileged: true
hostPID: true
hostNetwork: true
This will work flawlessly, and the POD has hostPID, hostNetwork and runs as root.
~ $ kubectl-user get pods -n unprivileged-user
NAME READY STATUS RESTARTS AGE
privileged-deploy-8878b565b-8466r 1/1 Running 0 24m
What can we do now? We can do some nasty things!
Let’s analyse the situation. If we enter the POD, we can see that we have access to all the Host’s processes (thanks to hostPID) and the main network (thanks to hostNetwork).
~ $ kubectl-user exec -ti -n unprivileged-user privileged-deploy-8878b565b-8466r -- sh
/ # ps aux | head -n 5
PID USER TIME COMMAND
1 root 0:05 /usr/lib/systemd/systemd --switched-root --system --deserialize 16
574 root 0:01 /usr/lib/systemd/systemd-journald
605 root 0:00 /usr/lib/systemd/systemd-udevd
631 root 0:02 /sbin/auditd
/ # ip addr | head -n 10
1: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP qlen 1000
link/ether 56:2f:49:03:90:d0 brd ff:ff:ff:ff:ff:ff
inet 192.168.122.21/24 brd 192.168.122.255 scope global eth0
valid_lft forever preferred_lft forever
Having root access, we can use the command nsenter to run programs in different namespaces. Which namespace you ask? Well we can use the namespace of PID 1!
/ # nsenter --mount=/proc/1/ns/mnt --net=/proc/1/ns/net --ipc=/proc/1/ns/ipc \
--uts=/proc/1/ns/uts --cgroup=/proc/1/ns/cgroup -- sh -c /bin/bash
[root@worker01 /]#
So now we are root on the host node. We escaped the pod and are now able to do whatever we want on the node.
This obviously is a huge hole in the cluster security, and we cannot put the cluster in the hands of anyone and just rely on their good will! Let’s try to set up the cluster better using the CIS Security Benchmark for Kubernetes.
Securing the Kubernetes Cluster
A notable mention to K3s is that it already has a number of security mitigations applied and turned on by default and will pass a number of the Kubernetes CIS controls without modification. Which is a huge plus for us!
We will follow the cluster hardening task in the accompanying Github project roles/k3s-deploy/tasks/cluster_hardening.yml
File Permissions
File permissions are already well set with K3s, but a simple task to ensure files and folders are respectively 0600 and 0700 ensures following the CIS Benchmark rules from 1.1.1 to 1.1.21 (File Permissions)
# CIS 1.1.1 to 1.1.21
- name: Cluster Hardening - Ensure folder permission are strict
command: |
find {{ item }} -not -path "*containerd*" -exec chmod -c go= {} \;
register: chmod_result
changed_when: "chmod_result.stdout != \"\""
with_items:
- /etc/rancher
- /var/lib/rancher
Systemd Hardening
Digging deeper we will first harden our Systemd Service using the isolation capabilities it provides:
File: /etc/systemd/system/k3s-server.service and /etc/systemd/system/k3s-agent.service
### Full configuration not displayed for brevity
[...]
###
# Sandboxing features
{%if 'libselinux' in ansible_facts.packages %}
AssertSecurity=selinux
ConditionSecurity=selinux
{% endif %}
LockPersonality=yes
PrivateTmp=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelTunables=yes
ProtectSystem=full
ReadWriteDirectories=/var/lib/ /var/run /run /var/log/ /lib/modules /etc/rancher/
This will prevent the spawned process from having write access outside of the designated directories, protects the rest of the system from unwanted reads, protects the Kernel Tunables and Logs and sets up a private Home and TMP directory for the process.
This ensures a minimum layer of isolation between the process and the host. A number of modifications on the host system will be needed to ensure correct operation, in particular setting up sysctl flags that would have been modified by the process instead.
vm.panic_on_oom=0
vm.overcommit_memory=1
kernel.panic=10
kernel.panic_on_oops=1
File: /etc/sysctl.conf
After this we will be sure that the K3s process will not modify the underlying system. Which is a huge win by itself
CIS Hardening Flags
We are now on the application level, and here K3s comes to meet us being already set up with sane defaults for file permissions and service setups.
1 – Restrict TLS Ciphers to the strongest one and FIPS-140 approved ciphers
SSL, in an appropriate environment should comply with the Federal Information Processing Standard (FIPS) Publication 140-2
--kube-apiserver-arg=tls-min-version=VersionTLS12 \
--kube-apiserver-arg=tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384 \
File: /etc/systemd/system/k3s-server.service
--kubelet-arg=tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384 \
File: /etc/systemd/system/k3s-server.service and /etc/systemd/system/k3s-agent.service
2 – Enable cluster secret encryption at rest
Where etcd encryption is used, it is important to ensure that the appropriate set of encryption providers is used.
--kube-apiserver-arg='encryption-provider-config=/etc/k3s-encryption.yaml' \
File: /etc/systemd/system/k3s-server.service
apiVersion: apiserver.config.K8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: {{ k3s_encryption_secret }}
- identity: {}
File: /etc/k3s-encryption.yaml
To generate an encryption secret just run
~ $ head -c 32 /dev/urandom | base64
3 – Enable Admission Plugins for Pod Security Policies and Network Policies
The runtime requirements to comply with the CIS Benchmark are centered around pod security (PSPs) and network policies. By default, K3s runs with the “NodeRestriction” admission controller. With the following we will enable all the Admission Plugins requested by the CIS Benchmark compliance:
--kube-apiserver-arg='enable-admission-plugins=AlwaysPullImages,DefaultStorageClass,DefaultTolerationSeconds,LimitRanger,MutatingAdmissionWebhook,NamespaceLifecycle,NodeRestriction,PersistentVolumeClaimResize,PodSecurityPolicy,Priority,ResourceQuota,ServiceAccount,TaintNodesByCondition,ValidatingAdmissionWebhook' \
File: /etc/systemd/system/k3s-server.service
4 – Enable APIs auditing
Auditing the Kubernetes API Server provides a security-relevant chronological set of records documenting the sequence of activities that have affected system by individual users, administrators or other components of the system
--kube-apiserver-arg=audit-log-maxage=30 \
--kube-apiserver-arg=audit-log-maxbackup=30 \
--kube-apiserver-arg=audit-log-maxsize=30 \
--kube-apiserver-arg=audit-log-path=/var/lib/rancher/audit/audit.log \
File: /etc/systemd/system/k3s-server.service
5 – Harden APIs
If –service-account-lookup is not enabled, the apiserver only verifies that the authentication token is valid, and does not validate that the service account token mentioned in the request is actually present in etcd. This allows using a service account token even after the corresponding service account is deleted. This is an example of time of check to time of use security issue.
Also APIs should never allow anonymous querying on either the apiserver or kubelet side.
--node-taint CriticalAddonsOnly=true:NoExecute \
File: /etc/systemd/system/k3s-server.service
6 – Do not schedule Pods on Masters
By default K3s does not distinguish between control-plane and nodes like full kubernetes does, and does schedule PODs even on master nodes.
This is not recommended on a production multi-node and multi-master environment so we will prevent this adding the following flag
--kube-apiserver-arg='service-account-lookup=true' \
--kube-apiserver-arg=anonymous-auth=false \
--kubelet-arg='anonymous-auth=false' \
--kube-controller-manager-arg='use-service-account-credentials=true' \
--kube-apiserver-arg='request-timeout=300s' \
--kubelet-arg='streaming-connection-idle-timeout=5m' \
--kube-controller-manager-arg='terminated-pod-gc-threshold=10' \
File: /etc/systemd/system/k3s-server.service
Where are we now?
We now have a quite well set up cluster both node-wise and service-wise, but are we done yet?
Not really, we have auditing and we have enabled a bunch of admission controllers, but the previous deployment still works because we are still missing an important piece of the puzzle.
PodSecurityPolicies
1 – Privileged Policies
First we will create a system-unrestricted PSP, this will be used by the administrator account and the kube-system namespace, for the legitimate privileged workloads that can be useful for the cluster.
Let’s define it in roles/k3s-deploy/files/policy/system-psp.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: system-unrestricted-psp
spec:
privileged: true
allowPrivilegeEscalation: true
allowedCapabilities:
- '*'
volumes:
- '*'
hostNetwork: true
hostPorts:
- min: 0
max: 65535
hostIPC: true
hostPID: true
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
So we are allowing PODs with this PSP to be run as root and can have hostIPC, hostPID and hostNetwork.
This will be valid only for cluster-nodes and for kube-system namespace, we will define the corresponding CusterRole and ClusterRoleBinding for these entities in the playbook.
2 – Unprivileged Policies
For the rest of the users and namespaces we want to limit the PODs capabilities as much as possible. We will provide the following PSP in roles/k3s-deploy/files/policy/restricted-psp.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: global-restricted-psp
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' # CIS - 5.7.2
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default' # CIS - 5.7.2
spec:
privileged: false # CIS - 5.2.1
allowPrivilegeEscalation: false # CIS - 5.2.5
requiredDropCapabilities: # CIS - 5.2.7/8/9
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
forbiddenSysctls:
- '*'
hostPID: false # CIS - 5.2.2
hostIPC: false # CIS - 5.2.3
hostNetwork: false # CIS - 5.2.4
runAsUser:
rule: 'MustRunAsNonRoot' # CIS - 5.2.6
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
fsGroup:
rule: 'MustRunAs'
ranges:
- min: 1
max: 65535
readOnlyRootFilesystem: false
We are now disallowing privileged containers, hostPID, hostIPD and hostNetwork, we are forcing the container to run with a non-root user and applying the default seccomp profile for docker containers, whitelisting only a restricted and well-known amount of syscalls in them.
We will create the corresponding ClusterRole and ClusterRoleBindings in the playbook, enforcing this PSP to any system:serviceaccounts, system:authenticated and system:unauthenticated.
3 – Disable default service accounts by default
We also want to disable automountServiceAccountToken for all namespaces. By default kubernetes enables it and any POD will mount the default service account token inside it in /var/run/secrets/kubernetes.io/serviceaccount/token. This is also dangerous as reading this will automatically give the attacker the possibility to query the kubernetes APIs being authenticated.
To remediate we simply run
- name: Fetch namespace names
shell: |
set -o pipefail
{{ kubectl_cmd }} get namespaces -A | tail -n +2 | awk '{print $1}'
changed_when: no
register: namespaces
# CIS - 5.1.5 - 5.1.6
- name: Security - Ensure that default service accounts are not actively used
command: |
{{ kubectl_cmd }} patch serviceaccount default -n {{ item }} -p \
'automountServiceAccountToken: false'
register: kubectl
changed_when: "'no change' not in kubectl.stdout"
failed_when: "'no change' not in kubectl.stderr and kubectl.rc != 0"
run_once: yes
with_items: "{{ namespaces.stdout_lines }}"
Final Result
In the end the cluster will adhere to the following CIS ruling
- CIS – 1.1.1 to 1.1.21 — File Permissions
- CIS – 1.2.1 to 1.2.35 — API Server setup
- CIS – 1.3.1 to 1.3.7 — Controller Manager setup
- CIS – 1.4.1, 1.4.2 — Scheduler Setup
- CIS – 3.2.1 — Control Plane Setup
- CIS – 4.1.1 to 4.1.10 — Worker Node Setup
- CIS – 4.2.1 to 4.2.13 — Kubelet Setup
- CIS – 5.1.1 to 5.2.9 — RBAC and Pod Security Policies
- CIS – 5.7.1 to 5.7.4 — General Policies
So now we have a cluster that is also fully compliant with the CIS Benchmark for Kubernetes. Did this have any effect?
Let’s try our POD escaping again
~ $ kubectl-user apply -f demo/privileged-deploy.yaml
deployment.apps/privileged-deploy created
~ $ kubectl-user get pods
No resources found in unprivileged-user namespace.
~ $ kubectl-user get rs
NAME DESIRED CURRENT READY AGE
privileged-deploy-8878b565b 1 0 0 108s
~ $ kubectl-user describe rs privileged-deploy-8878b565b | tail -n8
Conditions:
Type Status Reason
---- ------ ------
ReplicaFailure True FailedCreate
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 54s (x15 over 2m16s) replicaset-controller Error creating: pods "privileged-deploy-8878b565b-" is forbidden: PodSecurityPolicy: unable to admit pod: [spec.securityContext.hostNetwork: Invalid value: true: Host network is not allowed to be used spec.securityContext.hostPID: Invalid value: true: Host PID is not allowed to be used spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
So the POD is not allowed, PSPs are working!
We can even try this command that will not create a Replica Set but directly a POD and attach to it.
~ $ kubectl-user run hostname-sudo --restart=Never -it --image overriden --overrides '
{
"spec": {
"hostPID": true,
"hostNetwork": true,
"containers": [
{
"name": "busybox",
"image": "alpine:3.7",
"command": ["nsenter", "--mount=/proc/1/ns/mnt", "--", "sh", "-c", "exec /bin/bash"],
"stdin": true,
"tty": true,
"resources": {"requests": {"cpu": "10m"}},
"securityContext": {
"privileged": true
}
}
]
}
}' --rm --attach
Result will be
Error from server (Forbidden): pods "hostname-sudo" is forbidden: PodSecurityPolicy: unable to admit pod: [spec.securityContext.hostNetwork: Invalid value: true: Host network is not allowed to be used spec.securityContext.hostPID: Invalid value: true: Host PID is not allowed to be used spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
So we are now able to restrict unprivileged users from doing nasty stuff on our cluster.
What about the admin role? Does that command still work?
~ $ kubectl run hostname-sudo --restart=Never -it --image overriden --overrides '
{
"spec": {
"hostPID": true,
"hostNetwork": true,
"containers": [
{
"name": "busybox",
"image": "alpine:3.7",
"command": ["nsenter", "--mount=/proc/1/ns/mnt", "--", "sh", "-c", "exec /bin/bash"],
"stdin": true,
"tty": true,
"resources": {"requests": {"cpu": "10m"}},
"securityContext": {
"privileged": true
}
}
]
}
}' --rm --attach
If you don't see a command prompt, try pressing enter.
[root@worker01 /]#
Checkpoint
So we now have a hardened cluster from base OS to the application level, but as shown above some edge cases still make it insecure.
What we will analyse in the last and final part of this blog series is how to use Sysdig’s Falco security suite to cover even admin roles and RCEs inside PODs.
All the playbooks are available in the Github repo on https://github.com/digitalis-io/k3s-on-prem-production
Related Articles
K3s – lightweight kubernetes made ready for production – Part 3
Do you want to know securely deploy k3s kubernetes for production? Have a read of this blog and accompanying Ansible project for you to run.
K3s – lightweight kubernetes made ready for production – Part 1
Do you want to know securely deploy k3s kubernetes for production? Have a read of this blog and accompanying Ansible project for you to run.
Digitalis becomes a SUSE Gold Partner specialising in Rancher and Kubernetes
Digitalis is now a SUSE Gold Partner specialising in SUSE Rancher Kubernetes products and services