Immutable Runtime Containers – Kubernetes Security Hardening
In my other posts under Kubernetes Security Hardening (Guides > Kubernetes Security Hardening), we have talked about securing Kubernetes environment by securing the supply chain i.e. docker images, pods, etc. In this post, we will talk about runtime security.
What is Immutable Container?
By Immutable container, we mean that container cannot be modified during its lifetime.
If some get shell access to container and modify some file or install a package, then container becomes mutable and hence it drifts from its desired state during its lifetime. This means this is not the secure container anymore that we had deployed.
Hence it is important to enforce Immutability to a container running in Kubernetes Cluster.
Ways of Enforcing Immutability
There are few ways to enforce Immutability for containers. Mainly we make root file-system read only and remove shell or remove package manager or remove utilities like touch, nano editor, vim editor etc. We can do this at two levels:
- Enforce at Image Level: This part we have already done in our blog post on docker image hardening when we used distroless images as base images. To enforce immutability at Image level you can remove shell or make file system read-only (in Dockerfile using chmod to make root system read-only by removing write permissions), etc.
- Enforce at Runtime: In some scenario if you are for some reason not able to enforce immutability at image level, then we do this at POD level as well by using following:
- startUpProbe: You can use startUpProbe to run commands to remove shell or utilities and also make filesystem read only.
- initContainer: The initContainer run before application container. So you can use initContainer to perform actions that read-write permissions like updating a cache that your application container will use and then making file system of application readonly. This can be very specific and applicable to not many scenarios but is one way of running Immutable Containers.
- securityContext: You can use securityContext to set flag
readOnlyRootFilesystem
to true
Let’s do some hands on examples for enforcing Immutability at runtime for containers.
Set up Test Lab
Before we start hands on, let’s set up a test lab using kind and docker. Please follow below for setting up test Lab
Now we will start hands on.
Enforce Immutability with startUpProbe
startUpProbe is a feature provided by Kubernetes which is used to finish application start before other probes can run and application comes into READY state.
So we can use startUpProbe to make our container Immutable
Copy and paste the below code into apache-httpd.yaml file
apiVersion: v1
kind: Pod
metadata:
name: apache
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
startupProbe:
exec:
command:
- rm
- /bin/touch
initialDelaySeconds: 5
periodSeconds: 5
Deploy the Pod
# kubectl apply -f apache-httpd.yaml
Now we will try to create a file inside apache container using touch command
# kubectl get pod
NAME READY STATUS RESTARTS AGE
apache 1/1 Running 0 26s
# kubectl exec -it apache -- bash
root@apache:/usr/local/apache2# touch file
bash: touch: command not found
As you can see we could not create a file using touch. In this way you can remove any other utilities from stopping users or attackers to make any modifications.
Let’s try removing bash to stop users from getting shell access
Copy and paste this code to apache-httpd.yaml
apiVersion: v1
kind: Pod
metadata:
name: apache
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
startupProbe:
exec:
command:
- rm
- /bin/bash
initialDelaySeconds: 5
periodSeconds: 5
Let’s recreate the pod
# kubectl delete -f apache-httpd.yaml
pod "apache" deleted
# kubectl apply -f apache-httpd.yaml
pod/apache created
We will try accessing shell and see if it allows us
# kubectl get pods
NAME READY STATUS RESTARTS AGE
apache 1/1 Running 0 48s
# kubectl exec -it apache -- bash
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "d7559d0db5d99a44618cc467c4b59094364949502f81286a1976a2889114bf06": OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown
Awesome! We could not get shell access to container.
Enforce Immutability with securityContext
Like mentioned before we can use securityContext to make root filesystem secure
Copy and paste this yaml to apache-httpd.yaml
apiVersion: v1
kind: Pod
metadata:
name: apache
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
securityContext:
readOnlyRootFilesystem: true
Recreate the pod
# kubectl delete -f apache-httpd.yaml
pod "apache" deleted
# kubectl apply -f apache-httpd.yaml
pod/apache created
This will create the pod but it will fail
# kubectl get pods
NAME READY STATUS RESTARTS AGE
apache 0/1 CrashLoopBackOff 2 (17s ago) 41s
Why this happened?
You may have guessed it right. Because filesystem is read only. This will fail almost all of the applications.
How to fix Read only file system issue?
In order to fix read only filesystem issue we can mount emptyDir{}
volume to provide read/write mounts to applications and hence enabling us to keep the root file system read only.
Let’s check the logs of above created apache pod
# kubectl logs apache
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.244.0.33. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.244.0.33. Set the 'ServerName' directive globally to suppress this message
[Wed Jan 04 02:01:17.289092 2023] [core:error] [pid 1:tid 281473095491600] (30)Read-only file system: AH00099: could not create /usr/local/apache2/logs/httpd.pid.InrxVg
[Wed Jan 04 02:01:17.289152 2023] [core:error] [pid 1:tid 281473095491600] AH00100: httpd: could not log pid to file /usr/local/apache2/logs/httpd.pid
As you can see above it failed due to /usr/local/apache2/logs being read only. Let’s fix this now
Copy and paste below yaml to apache-httpd.yaml
apiVersion: v1
kind: Pod
metadata:
name: apache
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /usr/local/apache2/logs
name: apache-volume
volumes:
- name: apache-volume
emptyDir:
sizeLimit: 500Mi
Recreate the pod
# kubectl delete -f apache-httpd.yaml
pod "apache" deleted
# kubectl apply -f apache-httpd.yaml
pod/apache created
Let’s check the pod status
# kubectl get pods
NAME READY STATUS RESTARTS AGE
apache 1/1 Running 0 9s
# kubectl logs apache
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.244.0.34. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 10.244.0.34. Set the 'ServerName' directive globally to suppress this message
[Wed Jan 04 02:04:54.633015 2023] [mpm_event:notice] [pid 1:tid 281473599520784] AH00489: Apache/2.4.54 (Unix) configured -- resuming normal operations
[Wed Jan 04 02:04:54.633208 2023] [core:notice] [pid 1:tid 281473599520784] AH00094: Command line: 'httpd -D FOREGROUND'
Nice!! It is working now
Let’s check if filesystem other than /usr/local/apache2/logs
read only or not.
# kubectl exec -it apache -- bash
root@apache:/usr/local/apache2# pwd
/usr/local/apache2
root@apache:/usr/local/apache2# touch test
touch: cannot touch 'test': Read-only file system
Awesome!! We have read only Filesystem.
Conclusion
In this post we learnt how we can further improve our security. This will definitely additional layer of security and will make your system more difficult to crack. Happy learning!!