In this article I will run some basic tests in an Azure Kubernetes Services cluster configured with Azure CNI network plugin and Calico.
Background
By default, all network traffic is allowed between pods of a Kubernetes cluster. Pods can reach services everywhere in the cluster, even in other namespace.
What I’d like to test today is the possibility to isolate namespaces so that pods cannot connect from one namespace to another. We will try to do so with Calico network policies.
Prerequisites
-
An Azure account and subscription.
-
An AKS cluster with Azure CNI and Calico deployed (this can only happen at deployment so be carefull).
-
kubectl
andcalicoctl
available on your workstation. -
some spare time to test :)
Create 2 namespaces
Create the development
and test
namespaces in your cluster.
kubectl create namespace development
kubectl create namespace test
Add some labels on the namespaces. Labels will be used as selectors in the network policies we will deploy later on.
kubectl label namespace development env=dev
kubectl label namespace test env=test
Deploy whoami container in development
Use the following manifest to deploy whoami
application and service in development
namespace.
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
selector:
matchLabels:
app: whoami
replicas: 1
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami-container
image: containous/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
ports:
- name: http
targetPort: 80
port: 80
selector:
app: whoami
Deploy with kubectl
in development
namespace.
kubectl -n development apply -f whoami-deployment.yml
Check the status of the service.
kubectl -n development get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
whoami-service ClusterIP 10.10.10.18 <none> 80/TCP 6m21s
Test the service / pod deployment using kubectl port-forward
kubectl port-forward -n development service/whoami-service 8000:80
Forwarding from 127.0.0.1:8000 -> 80
Forwarding from [::1]:8000 -> 80
PS: The
kubectl port-forward
command does not return. It must stay in executing status for the port-forwarding to continue working. Cancel it withCTRL-C
.
Access service by opening a web browser to 127.0.0.1:8000
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 127.0.0.1:49890
GET / HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 Edg/87.0.664.75
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en,fr;q=0.9,fr-FR;q=0.8,en-GB;q=0.7,en-US;q=0.6,nl;q=0.5,de;q=0.4
Connection: keep-alive
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
Now that we know the container runs correctly, let’s spawn a new container to test from inside the cluster, in the same namespace.
kubectl run --rm -it --image=alpine testtool --namespace development
wget -qO- --timeout=2 http://whoami-service.development
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 10.240.0.32:39446
GET / HTTP/1.1
Host: whoami-service.development
User-Agent: Wget
Connection: close
The connection works.
Let’s try to also reach the service from the test
namespace.
kubectl run --rm -it --image=alpine testtool --namespace test
wget -qO- --timeout=2 http://whoami-service.development
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 10.240.0.32:39446
GET / HTTP/1.1
Host: whoami-service.development
User-Agent: Wget
Connection: close
It also works.
Our start situation is thus:
Now let’s try to apply some basic Calico network policies.
About Kubernetes and Calico network policies
Disclaimer: I’m far from being a master at Kubernetes networking stuff and what follows is only my current understanding of the subject
When Calico is deployed in a Kubernetes cluster, you can define 2 kinds of network policies: Kubernetes network policies and Calico network policies.
There are some differences between the two kinds (for more info see An overview of Kubernetes and Calico network policies) but you can define and apply both types of policies together.
The order of precedence to apply these policies is defined by a spec
of the network policy object named order
. Kubernetes network policies have a default order of 1000 and it cannot be specified / modified in the policy definition. Calico policies allow to specify an order in their definition. You should correctly define it to avoid wrong order of precedence if you combine both types of policies.
To avoid confusion, we will only work with Calico network policies in this article.
Another thing that you must have in mind is how the deny / allow traffic behaves.
- If no network policies apply to a pod, then all traffic to/from that pod is allowed.
- If one or more network policies apply to a pod containing ingress rules, then only the ingress traffic specifically allowed by those policies is allowed.
- If one or more network policies apply to a pod containing egress rules, then only the egress traffic specifically allowed by those policies is allowed.
For compatibility with Kubernetes, Calico network policy follows the same behavior for Kubernetes pods. For other endpoint types (VMs, host interfaces), Calico network policy is default deny. We will only configure network policies on pods.
Applying and testing policies
Network policies can make things complex so we will stick to some simple rules to bring more security in our cluster without too much complexity.
A good first step would be to deny everything in the development namespace. This policy will be the last one applied.
Create a Calico policy yaml file with the following content:
# Calico network policy that denies all traffic in development namespace
# Denies even traffic between pods in the same namespace !
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
name: default-deny
namespace: development
spec:
order: 5000
# Select all endpoints in the namespace
selector: all()
types:
- Ingress
# Default action if not specified is deny
- Egress
# Default action if not specified is deny
Apply the policy
calicoctl apply -f 01-deny-all.yaml
Now the test from development
namespace does not work anymore.
kubectl run --rm -it --image=alpine testtool --namespace development
wget -qO- --timeout=2 http://whoami-service.development
wget: bad address 'whoami-service.development'
This is normal because all traffic is denied by our policy, even the traffic to kube-dns
for name resolution inside the cluster. To solve that, let’s create and apply a new policy that will allow UDP egress traffic to kube-dns
pods in namespace kube-system
.
Policy content :
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
name: dns-allow
namespace: development
spec:
order: 4000
# Applies to all endpoints in the namespace
selector: all()
types:
- Egress
egress:
- action: Allow
protocol: UDP
destination:
# I tried to use projectcalico.org/name to select the namespace name but it did not work ...
namespaceSelector: control-plane == "true"
selector: 'k8s-app == "kube-dns"'
ports:
- 53
Apply:
calicoctl apply -f 02-allow-dns-egress.yaml
Test
kubectl run --rm -it --image=alpine testtool -n development
wget -qO- --timeout=2 http://whoami-service.development
wget: download timed out
So now name resolution is ok but connectivity inside the namespace is still not ok.
Let’s fix this by creating a new rule allowing all ingress and egress traffic from pods running inside development
namespace (remember that our default rule denies all traffic!).
# Calico network policy that allows ingress and egress traffic to all pods in development namespace
# from all pods in the SAME namespace
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
name: development-ns-allow
namespace: development
spec:
order: 500
# Applies to all endpoints in the namespace
selector: all()
types:
- Ingress
- Egress
ingress:
- action: Allow
protocol: TCP
source:
namespaceSelector: env == "dev"
egress:
- action: Allow
protocol: TCP
destination:
namespaceSelector: env == "dev"
Test from development
namespace
kubectl run --rm -it --image=alpine testtool -n development
wget -qO- --timeout=2 http://whoami-service.development
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 10.240.0.17:41100
GET / HTTP/1.1
Host: whoami-service.development
User-Agent: Wget
Connection: close
Finally it works as expected.
Now test from outside the namespace
# From test namespace
kubectl run --rm -it --image=alpine testtool -n test
wget -qO- --timeout=2 http://whoami-service.development
wget: download timed out
# From default namespace
kubectl run --rm -it --image=alpine testtool
wget -qO- --timeout=2 http://whoami-service.development
wget: download timed out
Both tests from outside are failing with timeouts which is correct due to our current configuration.
Let’s run a last test. Add a new policy to allow ingress traffic also from the test
namespace.
# Calico network policy that allows ingress traffic to all pods in development namespace
# from all pods in the test namespace
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
name: test-ns-allow
namespace: development
spec:
order: 600
# Applies to all endpoints in the development namespace
selector: all()
types:
- Ingress
ingress:
- action: Allow
protocol: TCP
source:
namespaceSelector: env == "test"
Apply the policy.
calicoctl apply -f 04-allow-ingress-from-test.yaml
Test from development
namespace -> Still OK.
kubectl run --rm -it --image=alpine testtool -n development
wget -qO- --timeout=2 http://whoami-service.development
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 10.240.0.18:42046
GET / HTTP/1.1
Host: whoami-service.development
User-Agent: Wget
Connection: close
Test from test
namespace -> Now OK too.
kubectl run --rm -it --image=alpine testtool -n test
wget -qO- --timeout=2 http://whoami-service.development
Hostname: whoami-deployment-5d4bfbc5d7-cph77
IP: 127.0.0.1
IP: 10.240.0.11
RemoteAddr: 10.240.0.18:42046
GET / HTTP/1.1
Host: whoami-service.development
User-Agent: Wget
Connection: close
Test from default
namespace -> Still fails but that is expected.
kubectl run --rm -it --image=alpine testtool
wget -qO- --timeout=2 http://whoami-service.development
wget: download timed out
The final situation is:
Check the policies existing in the namespace.
calicoctl get networkpolicies -n development
NAMESPACE NAME
development default-deny
development development-ns-allow
development dns-allow
development test-ns-allow
Cleanup resources and conclusion
Now that the tests are finished, delete all resources.
kubectl delete namespace test
kubectl delete namespace development
As you have read (I hope you survived …) in this rather long article, Calico network policies allow us to isolate network traffic inside a namespace and still extend to traffic coming from other namespaces.
This gives us the possibility to secure the network flows inside our Kubernetes cluster and make our security engineers happier.
See you soon.