Enabling Authorization to our API
At the moment, any service in the mesh can access our demo application as long as it uses mTLS. However, in a real-world scenario, we would like to restrict access to our demo application even further. Without service meshes, we could use Network Policies to restrict access to our services. However, Network Policies only work at the network level and do not provide any application-level security. Therefore, this application-level security would need to be implemented in each service individually. By using a service mesh like Istio, we can define authorization rules that are enforced by the sidecar proxies. This way, we can restrict access to our demo application based on the identity of the services.
In our demo application, we will restrict access to the API service. Only services in the learning-tracker namespace should be allowed to access the /api endpoints of the API service, and all other services should be able to access the /health endpoint. To do this, we will create an AuthorizationPolicy resource in Istio.
This resource should look like this:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: authorize-api
namespace: learning-tracker
spec:
action: ALLOW
selector:
matchLabels:
app: learning-tracker-api
rules:
- from:
- source:
namespaces: ["learning-tracker","istio-system"]
to:
- operation:
paths: ["/api/*"]
methods: ["POST","GET","DELETE"]
- to:
- operation:
paths: ["/health"]
This resource allows access to the /api/* endpoints of the API service only from services in the learning-tracker and istio-system namespaces. All other services are denied access to these endpoints. The /health endpoint is accessible from all services. The selector field specifies that this policy applies only to the API service. The action field specifies that this is an allow policy, meaning that only the specified rules are allowed, and all other traffic is denied.
Create a file named authorization-policy.yaml with the above content and apply it using the command:
kubectl apply -f authorization-policy.yaml
After applying the policy, you can verify that it works by trying to access the API service from the trusted-test pod:
kubectl exec -it trusted-test -n trusted -- /bin/bash
curl http://learning-tracker-api.learning-tracker.svc.cluster.local/api/health # should not work
curl http://learning-tracker-api.learning-tracker.svc.cluster.local/health # should work
As you can see, the /api/health endpoint is not accessible from the trusted-test pod, while the /health endpoint is accessible. This is because the trusted-test pod is not in the learning-tracker or istio-system namespaces, and therefore it is denied access to the /api/* endpoints. On the other hand, you can still port-forward to the frontend service and the web application should still work as expected. This is because the frontend service is in the learning-tracker namespace and is therefore allowed to access the /api/* endpoints of the API service.
Our web application is still behaving weirdly. Don't worry, this is still intended.
With this, we have successfully restricted access to our demo application using Istio authorization policies. In the next step, we will use the istio-ingressgateway to expose our demo application to the outside world.
Exposing our Application using the Istio Ingress Gateway
At the moment, our demo application is only accessible from within the cluster. To make it accessible from the outside world, we typically use an Ingress Controller or Gateway. Istio comes with its own Ingress Gateway that can be used to expose services to the outside world. The Istio Ingress Gateway is a Kubernetes Service of type LoadBalancer that is managed by Istio. It uses the Envoy proxy as a reverse proxy to route traffic to the appropriate services in the cluster based on the defined rules. To expose our demo application using the Istio Ingress Gateway, we need to create a Gateway and a VirtualService resource in Istio.
At first, we need to know the external IP address of the Istio Ingress Gateway. To get this, run the following command:
kubectl get svc istio-ingressgateway -n istio-system -ojsonpath='{.status.loadBalancer.ingress[0].ip}'
This command will return the external IP address of the Istio Ingress Gateway. If you are using a managed Kubernetes service, it might take a few minutes for the external IP address to be assigned. If you are using a local Kubernetes cluster (e.g., Minikube, Kind, etc.), you might need to use a different method to access the Istio Ingress Gateway, such as using minikube tunnel or kubectl port-forward.
Once you have the external IP address, you can create a Gateway and a VirtualService resource to expose the frontend service of our demo application. Before creating these resources, we need to know what Gateways and Virtual Services are:
- Gateway: A Gateway defines how traffic enters the service mesh. It specifies the ports, protocols, and other settings for incoming traffic. A Gateway can be thought of as a load balancer that routes traffic to the appropriate services in the mesh.
- VirtualService: A VirtualService defines how traffic is routed to the services in the mesh. It specifies the rules for routing traffic based on the host, path, headers, and other criteria. A VirtualService can be thought of as a set of routing rules that determine how traffic is directed to the appropriate services.
To create a Gateway, create a file named gateway.yaml with the following content:
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
name: learning-tracker
namespace: learning-tracker
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 8080
name: http
protocol: HTTP
hosts:
- "<external-gateway-ip>.nip.io"
This Gateway defines a single server that listens on port 8080 for HTTP traffic. The hosts field specifies the hostname that the Gateway will respond to. In this case, we are using a wildcard DNS service called nip.io that maps any subdomain to the specified IP address. Replace <external-gateway-ip> with the external IP address of the Istio Ingress Gateway that you obtained earlier. The selector field specifies the gateway to use. You can find the correct selector by running kubectl get svc istio-ingressgateway -n istio-system -o yaml and looking for istio label.
Secondly, we need to create a VirtualService to route traffic to the frontend service. Create a file named vs-frontend.yaml with the following content:
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: learning-tracker-frontend
namespace: learning-tracker
spec:
hosts:
- "<your-ip>.nip.io"
- "learning-tracker-frontend"
gateways:
- learning-tracker
http:
- name: "learning-tracker-frontend"
match:
- uri:
prefix: "/"
route:
- destination:
host: learning-tracker-frontend
port:
number: 80
This VirtualService defines a single HTTP route that matches all requests to the root path (/) and routes them to the learning-tracker-frontend service on port 80. The hosts field specifies the hostname that the VirtualService will respond to. Again, replace <your-ip> with the external IP address of the Istio Ingress Gateway.
Finally, apply both resources using the following commands:
kubectl apply -f gateway.yaml
kubectl apply -f vs-frontend.yaml
After applying the resources, you should be able to access the frontend service of our demo application using the external IP address of the Istio Ingress Gateway. Open your browser and navigate to http://<external-gateway-ip>.nip.io:8080. You should see the frontend of the demo application. You can also verify that the application is working by adding new learning goals and moving them through the statuses "In Progress" and "Completed."
You have successfully exposed the frontend service of our demo application using the Istio Ingress Gateway. In the next step, we will configure a canary deployment using Argo Rollouts to gradually shift traffic between different versions of the frontend and backend service. Therefore, we will use Argo Rollouts to manage the deployments and Istio to manage the traffic routing.
