--- title: 'CKA Practice: Configure Network Policies To Restrict Traffic Between Pods' description: | This exercise tests your ability to configure kubernetes network policies to make sure only pods with specific labels can communicate with each other. kind: challenge playground: name: k3s tabs: - machine: dev-machine - machine: cplane-01 - machine: node-01 machines: - name: dev-machine users: - name: root default: true welcome: | Welcome to the CKA networking exercise. Follow the instructions to configure secure pod communication. - name: cplane-01 - name: node-01 cover: __static__/pod-networking-with-labels.png createdAt: 2024-11-03 updatedAt: 2026-08-21 difficulty: easy categories: - kubernetes - networking tagz: - cka - network-policies tasks: verify_namespace: machine: dev-machine timeout_seconds: 10 run: | for ATTEMPT in 1 2 3; do PHASE=$(kubectl get namespace app -o jsonpath='{.status.phase}' 2>/dev/null) || PHASE="" if [ "$PHASE" = "Active" ]; then echo "Namespace created!" exit 0 fi if [ "$ATTEMPT" != "3" ]; then sleep 2 fi done echo "Namespace 'app' was not found or is not Active." >&2 exit 1 verify_deployment_frontend: machine: dev-machine needs: - verify_namespace run: | EXPECTED_IMAGE="ghcr.io/iximiuz/labs/nginx:alpine" EXPECTED_PORT="80" for ATTEMPT in 1 2 3; do STATE=$(kubectl get deployment frontend -n app -o jsonpath='{.metadata.generation}{"|"}{.status.observedGeneration}{"|"}{.spec.replicas}{"|"}{.status.updatedReplicas}{"|"}{.status.readyReplicas}{"|"}{.status.availableReplicas}{"|"}{range .spec.template.spec.containers[*]}{";"}{.image}{"@"}{range .ports[*]}{.containerPort}{","}{end}{end}' 2>/dev/null) || STATE="" IFS='|' read -r GENERATION OBSERVED DESIRED UPDATED READY AVAILABLE CONTAINERS <&2 exit 1 verify_deployment_backend: machine: dev-machine needs: - verify_namespace run: | EXPECTED_IMAGE="ghcr.io/lpmi-13/default-go" EXPECTED_PORT="8000" for ATTEMPT in 1 2 3; do STATE=$(kubectl get deployment backend -n app -o jsonpath='{.metadata.generation}{"|"}{.status.observedGeneration}{"|"}{.spec.replicas}{"|"}{.status.updatedReplicas}{"|"}{.status.readyReplicas}{"|"}{.status.availableReplicas}{"|"}{range .spec.template.spec.containers[*]}{";"}{.image}{"@"}{range .ports[*]}{.containerPort}{","}{end}{end}' 2>/dev/null) || STATE="" IFS='|' read -r GENERATION OBSERVED DESIRED UPDATED READY AVAILABLE CONTAINERS <&2 exit 1 verify_labels_all_frontend: machine: dev-machine needs: - verify_deployment_frontend run: | for ATTEMPT in 1 2 3; do SELECTOR=$(kubectl get deployment frontend -n app -o go-template='{{range $key, $value := .spec.selector.matchLabels}}{{printf "%s=%s," $key $value}}{{end}}' 2>/dev/null) || SELECTOR="" SELECTOR=${SELECTOR%,} if [ -n "$SELECTOR" ]; then ALL_PODS=$(kubectl get pods -n app -l "$SELECTOR" -o name 2>/dev/null) || ALL_PODS="" LABELED_PODS=$(kubectl get pods -n app -l "$SELECTOR,role=frontend" -o name 2>/dev/null) || LABELED_PODS="" ALL_COUNT=$(printf '%s\n' "$ALL_PODS" | awk 'NF { count++ } END { print count + 0 }') LABELED_COUNT=$(printf '%s\n' "$LABELED_PODS" | awk 'NF { count++ } END { print count + 0 }') if [ "$ALL_COUNT" = "2" ] && [ "$LABELED_COUNT" = "2" ]; then echo "Frontend pod labels configured correctly!" exit 0 fi fi sleep 1 done echo "Both pods selected by deployment 'frontend' must have label role=frontend." >&2 exit 1 verify_labels_all_backend: machine: dev-machine needs: - verify_deployment_backend run: | for ATTEMPT in 1 2 3; do SELECTOR=$(kubectl get deployment backend -n app -o go-template='{{range $key, $value := .spec.selector.matchLabels}}{{printf "%s=%s," $key $value}}{{end}}' 2>/dev/null) || SELECTOR="" SELECTOR=${SELECTOR%,} if [ -n "$SELECTOR" ]; then ALL_PODS=$(kubectl get pods -n app -l "$SELECTOR" -o name 2>/dev/null) || ALL_PODS="" LABELED_PODS=$(kubectl get pods -n app -l "$SELECTOR,tier=api" -o name 2>/dev/null) || LABELED_PODS="" ALL_COUNT=$(printf '%s\n' "$ALL_PODS" | awk 'NF { count++ } END { print count + 0 }') LABELED_COUNT=$(printf '%s\n' "$LABELED_PODS" | awk 'NF { count++ } END { print count + 0 }') if [ "$ALL_COUNT" = "2" ] && [ "$LABELED_COUNT" = "2" ]; then echo "Backend pod labels configured correctly!" exit 0 fi fi sleep 1 done echo "Both pods selected by deployment 'backend' must have label tier=api." >&2 exit 1 verify_labels_one_backend: machine: dev-machine needs: - verify_labels_all_backend run: | for ATTEMPT in 1 2 3; do SELECTOR=$(kubectl get deployment backend -n app -o go-template='{{range $key, $value := .spec.selector.matchLabels}}{{printf "%s=%s," $key $value}}{{end}}' 2>/dev/null) || SELECTOR="" SELECTOR=${SELECTOR%,} if [ -n "$SELECTOR" ]; then WITH_ROLE=$(kubectl get pods -n app -l "$SELECTOR,tier=api,role=backend" -o name 2>/dev/null) || WITH_ROLE="" WITHOUT_ROLE=$(kubectl get pods -n app -l "$SELECTOR,tier=api,!role" -o name 2>/dev/null) || WITHOUT_ROLE="" WITH_ROLE_COUNT=$(printf '%s\n' "$WITH_ROLE" | awk 'NF { count++ } END { print count + 0 }') WITHOUT_ROLE_COUNT=$(printf '%s\n' "$WITHOUT_ROLE" | awk 'NF { count++ } END { print count + 0 }') if [ "$WITH_ROLE_COUNT" = "1" ] && [ "$WITHOUT_ROLE_COUNT" = "1" ]; then echo "Single backend pod label configured correctly!" exit 0 fi fi sleep 1 done echo "Exactly one backend pod must have role=backend, and the other must not have a role label." >&2 exit 1 verify_network_policies: machine: dev-machine needs: - verify_labels_all_frontend - verify_labels_all_backend - verify_labels_one_backend # Denied connections are expected to time out, so leave enough time to test every pod. timeout_seconds: 60 run: | get_deployment_selector() { kubectl get deployment "$1" -n app -o go-template='{{range $key, $value := .spec.selector.matchLabels}}{{printf "%s=%s," $key $value}}{{end}}' 2>/dev/null } get_pod_entries() { kubectl get pods -n app -l "$1" -o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.status.podIP}{"|"}{.metadata.labels.role}{"|"}{.metadata.labels.tier}{"|"}{.status.phase}{"|"}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{"\n"}{end}' 2>/dev/null } count_entries() { printf '%s\n' "$1" | awk 'NF { count++ } END { print count + 0 }' } probe_http() { SOURCE_POD=$1 TARGET_IP=$2 TARGET_PORT=$3 kubectl exec -n app "$SOURCE_POD" -- sh -c ' if ! command -v curl >/dev/null 2>&1; then printf "127|000\n" exit 0 fi HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" --connect-timeout 1 --max-time 3 "$1" 2>/dev/null) CURL_STATUS=$? printf "%s|%s\n" "$CURL_STATUS" "$HTTP_CODE" ' sh "http://$TARGET_IP:$TARGET_PORT" 2>/dev/null } expect_allowed() { SOURCE_POD=$1 TARGET_IP=$2 TARGET_PORT=$3 DESCRIPTION=$4 RESULT="" for ATTEMPT in 1 2; do if RESULT=$(probe_http "$SOURCE_POD" "$TARGET_IP" "$TARGET_PORT") && [ "$RESULT" = "0|200" ]; then return 0 fi sleep 1 done echo "Expected $DESCRIPTION to be allowed with HTTP 200, but the probe returned '${RESULT:-exec failed}'." >&2 return 1 } expect_denied() { SOURCE_POD=$1 TARGET_IP=$2 TARGET_PORT=$3 DESCRIPTION=$4 if ! RESULT=$(probe_http "$SOURCE_POD" "$TARGET_IP" "$TARGET_PORT"); then echo "Could not execute the $DESCRIPTION probe from pod '$SOURCE_POD'." >&2 return 1 fi CURL_STATUS=${RESULT%%|*} HTTP_CODE=${RESULT#*|} case "$CURL_STATUS" in ''|*[!0-9]*) echo "The $DESCRIPTION probe returned an invalid result: '$RESULT'." >&2 return 1 ;; esac if [ "$CURL_STATUS" -ne 0 ] && [ "$CURL_STATUS" -ne 127 ] && [ "$HTTP_CODE" = "000" ]; then return 0 fi echo "Expected $DESCRIPTION to be denied, but the probe returned '$RESULT'." >&2 return 1 } FRONTEND_SELECTOR=$(get_deployment_selector frontend) || FRONTEND_SELECTOR="" BACKEND_SELECTOR=$(get_deployment_selector backend) || BACKEND_SELECTOR="" FRONTEND_SELECTOR=${FRONTEND_SELECTOR%,} BACKEND_SELECTOR=${BACKEND_SELECTOR%,} if [ -z "$FRONTEND_SELECTOR" ] || [ -z "$BACKEND_SELECTOR" ]; then echo "Both deployments must have a matchLabels pod selector." >&2 exit 1 fi PODS_READY=false for ATTEMPT in 1 2 3; do FRONTEND_ENTRIES=$(get_pod_entries "$FRONTEND_SELECTOR") || FRONTEND_ENTRIES="" BACKEND_ENTRIES=$(get_pod_entries "$BACKEND_SELECTOR") || BACKEND_ENTRIES="" VALID_FRONTENDS=$(printf '%s\n' "$FRONTEND_ENTRIES" | awk -F '|' '$2 != "" && $3 == "frontend" && $5 == "Running" && $6 == "True" { print $1 "|" $2 }') BACKEND_WITH_ROLE=$(printf '%s\n' "$BACKEND_ENTRIES" | awk -F '|' '$2 != "" && $3 == "backend" && $4 == "api" && $5 == "Running" && $6 == "True" { print $1 "|" $2 }') BACKEND_WITHOUT_ROLE=$(printf '%s\n' "$BACKEND_ENTRIES" | awk -F '|' '$2 != "" && $3 == "" && $4 == "api" && $5 == "Running" && $6 == "True" { print $1 "|" $2 }') if [ "$(count_entries "$VALID_FRONTENDS")" = "2" ] && \ [ "$(count_entries "$BACKEND_WITH_ROLE")" = "1" ] && \ [ "$(count_entries "$BACKEND_WITHOUT_ROLE")" = "1" ]; then PODS_READY=true break fi sleep 1 done if [ "$PODS_READY" != "true" ]; then echo "Could not find all expected Running and Ready frontend and backend pods." >&2 exit 1 fi # Every frontend pod must reach both backend pods on port 8000. for FRONTEND_ENTRY in $VALID_FRONTENDS; do FRONTEND_POD=${FRONTEND_ENTRY%%|*} for BACKEND_ENTRY in $BACKEND_WITH_ROLE $BACKEND_WITHOUT_ROLE; do BACKEND_POD=${BACKEND_ENTRY%%|*} BACKEND_IP=${BACKEND_ENTRY#*|} expect_allowed "$FRONTEND_POD" "$BACKEND_IP" 8000 "frontend '$FRONTEND_POD' -> backend '$BACKEND_POD'" || exit 1 done done # The authorized backend pod must reach every frontend pod on port 80. AUTHORIZED_BACKEND_POD=${BACKEND_WITH_ROLE%%|*} for FRONTEND_ENTRY in $VALID_FRONTENDS; do FRONTEND_POD=${FRONTEND_ENTRY%%|*} FRONTEND_IP=${FRONTEND_ENTRY#*|} expect_allowed "$AUTHORIZED_BACKEND_POD" "$FRONTEND_IP" 80 "authorized backend '$AUTHORIZED_BACKEND_POD' -> frontend '$FRONTEND_POD'" || exit 1 done # The unauthorized backend pod must not reach either frontend pod. UNAUTHORIZED_BACKEND_POD=${BACKEND_WITHOUT_ROLE%%|*} for FRONTEND_ENTRY in $VALID_FRONTENDS; do FRONTEND_POD=${FRONTEND_ENTRY%%|*} FRONTEND_IP=${FRONTEND_ENTRY#*|} expect_denied "$UNAUTHORIZED_BACKEND_POD" "$FRONTEND_IP" 80 "unauthorized backend '$UNAUTHORIZED_BACKEND_POD' -> frontend '$FRONTEND_POD'" || exit 1 done # Representative same-tier paths must also be denied. set -- $VALID_FRONTENDS FIRST_FRONTEND_POD=${1%%|*} SECOND_FRONTEND_IP=${2#*|} expect_denied "$FIRST_FRONTEND_POD" "$SECOND_FRONTEND_IP" 80 "frontend -> frontend traffic" || exit 1 UNAUTHORIZED_BACKEND_IP=${BACKEND_WITHOUT_ROLE#*|} expect_denied "$AUTHORIZED_BACKEND_POD" "$UNAUTHORIZED_BACKEND_IP" 8000 "backend -> backend traffic" || exit 1 echo "Network policies configured correctly!" exit 0 --- In this exercise, you will configure network policies to control traffic flow between two deployments in a Kubernetes cluster. You'll need to ensure specific pods can communicate based on their labels while blocking unauthorized traffic. Have fun! Diagram showing desired network policy configuration between frontend and backend pods First, create a namespace called "app": ::simple-task --- :tasks: tasks :name: verify_namespace --- #active Waiting for namespace to be created... #completed Good, the namespace is ready to go. :: ::hint-box --- :summary: Hint 1 --- Check the [documentation for creating namespaces](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#-em-namespace-em-) :: Now, create two deployments in that namespace. 1. Frontend: - Deployment "frontend" with 2 replicas running `ghcr.io/iximiuz/labs/nginx:alpine` - Declares container port 80 and serves HTTP on it 2. Backend: - Deployment "backend" with 2 replicas running `ghcr.io/lpmi-13/default-go` - Declares container port 8000 and serves HTTP on it ::simple-task --- :tasks: tasks :name: verify_deployment_frontend --- #active Waiting for frontend deployment to be created... #completed Great! Frontend deployment is running correctly. :: ::simple-task --- :tasks: tasks :name: verify_deployment_backend --- #active Waiting for backend deployment to be created... #completed Alright! Backend deployment is running correctly. :: ::hint-box --- :summary: Hint 2 --- Check the [documentation for creating deployments](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#-em-deployment-em-) :: Now, label the pods: 1. All frontend pods should have: `role=frontend` 2. All backend pods should have: `tier=api` 3. Only one backend pod should have the additional label: `role=backend` ::simple-task --- :tasks: tasks :name: verify_labels_all_frontend --- #active Checking for correct frontend pod labels... #completed Nice! Both frontend pods are properly labeled with `role=frontend`. :: ::simple-task --- :tasks: tasks :name: verify_labels_all_backend --- #active Checking for correct backend pod labels... #completed Fantastic! Both backend pods are properly labeled with `tier=api`. :: ::simple-task --- :tasks: tasks :name: verify_labels_one_backend --- #active Checking for correct backend pod label... #completed Good! Only one backend pod is properly labeled with `role=backend`. :: ::hint-box --- :summary: Hint 3 --- Check the [documentation for adding labels](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#label) :: ::hint-box --- :summary: Hint 4 --- ```bash # Get the name of one backend pod BACKEND_POD=$(kubectl get pods -n app -l app=backend -o jsonpath='{.items[0].metadata.name}') ``` :: Finally, create network policies to make sure: 1. All frontend pods can send traffic to any backend pod with label `tier=api` on port 8000 2. Only the backend pod with label `role=backend` can send traffic to frontend pods with label `role=frontend` on port 80 3. All other ingress traffic to the frontend and backend pods is denied ::simple-task --- :tasks: tasks :name: verify_network_policies --- #active Verifying network policies and testing connectivity... #completed Excellent! The network policies are correctly configured and enforcing the desired traffic patterns. :: ::hint-box --- :summary: Hint 5 --- Create two network policies, one for the frontend => backend, and another from the backend => frontend. Here's the first one: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: frontend-to-backend namespace: app spec: podSelector: matchLabels: tier: api policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: role: frontend ports: - port: 8000 protocol: TCP ``` ::