3906 words
20 minutes
Create an unmanaged gateway controller in AKS with NGINX Gateway Fabric

A gateway controller is the central component that implements the Gateway API, responsible for managing all Gateway resources. With the Gateway API, you can not only route traffic like you would with Ingress, but you also have better control over Gateway API resources. When you use a gateway controller and Gateway API resources, one or more IP addresses can be used to route traffic to multiple services in a Kubernetes cluster.

This article shows you how to deploy the NGINX Gateway Fabric in an Azure Kubernetes Service (AKS) cluster. Two applications will run in different namespaces within the AKS cluster, both accessible via a single IP address. Furthermore, an SSL/TLS certificate will be configured using Kubernetes Secret, cert-manager or Azure Key Vault Provider for Secrets Store CSI Driver.

To learn more about differences between the Ingress API and the Gateway API, see also: Migrating from Ingress.

Before you begin#

  • Prepare a domain for accessing the demo application. If you don’t have any, you can try free DDNS service like No-IP.

Package/API/add-on preparation#

First, we need to install all the necessary packages and APIs before proceeding with the configuration.

  1. Install the Gateway API There are two ways to install APIs:
    a. Refer to the Kubernetes official documentation to install the latest version of the Gateway API.
    b. If you prefer to use the Azure managed gateway API, you can also refer to the Azure documentation: Install Managed Gateway API Custom Resource Definitions (CRDs)
NOTE

However, note that some of the Gateway APIs are still not in General Availability (GA) and are subject to change. When using the managed Gateway API, consider the potential impact of version changes when upgrading your AKS cluster version.

TIP

Before using Azure managed Gateway API, you probably want to read the following sections under support policies (Archive: archive.today):

  • Unsupported alpha and beta Kubernetes features (Key point: AKS only supports stable and beta features within the upstream Kubernetes project)
  • Preview features or feature flags (Key point: features in public preview fall under best effort support)
  • Upstream bugs and issues
  1. Install NGINX Gateway Fabric (gateway controller) as below:
Terminal window
helm upgrade --install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway
TIP

Note that we will use NGINX Gateway Fabric as the gateway controller in this example. If you want to use a different gateway controller, check out: Gateway Controller Implementation Status

To use NodePort instead of LoadBalancer, using command below:

Terminal window
helm upgrade --install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginx.service.type=NodePort
  1. To generate publicly-trusted SSL/TLS certificate, install cert-manager using the command below:
Terminal window
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager --create-namespace --namespace cert-manager \
--set crds.enabled=true \
--set config.enableGatewayAPI=true
  1. If you need to use the Secrets Store CSI Driver to reference the certificate in Azure Key Vault, refer to Use the Azure Key Vault Provider for Secrets Store CSI Driver in an Azure Kubernetes Service (AKS) cluster for installation and permission-granting instructions.

Create a simple Gateway#

Use the following manifest to create a gateway within the “gateway-example” namespace:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway-public
namespace: gateway-example
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP

If you want to use Azure internal load balancer, use the following manifest:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway-internal
namespace: gateway-example
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
infrastructure:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: 'true'

Your Gateways will be assigned different IPs shortly. To check the IPs, use the command below:

Terminal window
kubectl get svc -n gateway-example

The output should be as follows:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
gateway-internal-nginx LoadBalancer 10.0.95.126 10.208.0.33 80:32550/TCP 5m26s
gateway-public-nginx LoadBalancer 10.0.175.217 123.45.67.89 80:30124/TCP 5m26s

Showcase: routing traffic to applications in different namespaces#

After understanding how to create Gateway with public and internal IPs, we will attempt to use the Gateway API in a sample case.

NOTE

The previously created Gateways mentioned above will not be utilized in the following demonstration. Instead, a new Gateway will be created.

Run demo applications#

To see the gateway controller in action, run two demo applications in your AKS cluster. In this example, you will use kubectl apply to deploy two instances of a simple Hello world application.

  1. Create namespace called “default-service-and-gateway” for gateway resource and default application deployment
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
EOF
  1. Deploy default appliation (service-one) in “default-service-and-gateway” namespace
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
spec:
replicas: 1
9 collapsed lines
selector:
matchLabels:
app.kubernetes.io/name: service-one
app.kubernetes.io/part-of: default-service
template:
metadata:
labels:
app.kubernetes.io/name: service-one
app.kubernetes.io/part-of: default-service
spec:
containers:
- name: service-one-app
image: mcr.microsoft.com/azuredocs/aks-helloworld:v1
ports:
- containerPort: 80
env:
- name: TITLE
value: "Welcome to AKS Default Service Page"
---
apiVersion: v1
kind: Service
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app.kubernetes.io/name: service-one
app.kubernetes.io/part-of: default-service
EOF
  1. Create namespace called “external-service” for another application (service-two)
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: external-service
labels:
app.kubernetes.io/part-of: external-service
EOF
  1. Deploy another appliation (service-two) in “external-service” namespace
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-two
namespace: external-service
labels:
app.kubernetes.io/part-of: external-service
spec:
replicas: 1
9 collapsed lines
selector:
matchLabels:
app.kubernetes.io/name: service-two
app.kubernetes.io/part-of: external-service
template:
metadata:
labels:
app.kubernetes.io/name: service-two
app.kubernetes.io/part-of: external-service
spec:
containers:
- name: service-two-app
image: mcr.microsoft.com/azuredocs/aks-helloworld:v1
ports:
- containerPort: 80
env:
- name: TITLE
value: "Welcome to AKS External Service Page"
---
apiVersion: v1
kind: Service
metadata:
name: service-two
namespace: external-service
labels:
app.kubernetes.io/part-of: external-service
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app.kubernetes.io/name: service-two
app.kubernetes.io/part-of: external-service
EOF

Create Gateway#

Both applications are now running in your Kubernetes cluster. Next, we will create a Gateway resource using the NGINX Gateway Fabric.

  1. Set your hostname as variable
Terminal window
# Set your hostname
routeDomain=<your-own-domain>
  1. Deploy Gateway resource
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: the-sole-unique-gateway
namespace: default-service-and-gateway
labels:
app.kubernetes.io/component: gateway-api
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
# Optional: set hostname
hostname: ${routeDomain}
infrastructure:
labels:
app.kubernetes.io/part-of: gateway-api
EOF
NOTE

The “hostname” field is optional; however, it is still being used in this demonstration as I want to add a sense of seriousness. I hope you won’t be misled to think that “hostname must be used”.

  1. Set up DNS record using the public IP you received
    You will need to set up the DNS record on your own. To obtain the current public IP used for Gateway, use the command below:
Terminal window
kubectl get gateway the-sole-unique-gateway -n default-service-and-gateway -o jsonpath='{.status.addresses[0].value}'

Create HTTPRoute#

To route traffic to each application, create a Kubernetes HTTPRoute resource. The HTTPRoute resource configures the rules that route traffic to one of the two applications.

In the following example, HTTPRoute will be created in the samespace with Gateway resource. Traffic to

  • ${routeDomain}/default is routed to the service named service-one (which is in the same namespace with Gateway resource).
  • ${routeDomain}/external is routed to the service named service-two (which is in a different namespace).
  • ${routeDomain}/static is routed to the service named service-one for static assets.
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /default
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-two
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: external-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /external
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-two
namespace: external-service
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one-static
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /static
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
EOF

Create ReferenceGrant#

Since service-two is located in a different namespace than the HTTPRoute resource, we need to use “ReferenceGrant” to enable cross namespace references within Gateway API.

Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: external-service-reference
namespace: external-service
labels:
app.kubernetes.io/part-of: external-service
app.kubernetes.io/component: gateway-api
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: default-service-and-gateway
to:
- group: ""
kind: Service
name: service-two
EOF
NOTE

There is no need to create “ReferenceGrant” resource for service-one, as its HTTPRoute and Service resources reside within the same namespace.

TIP

When the application is deployed in a different namespace than the Gateway resource, you can alternatively choose to allow the Gateway to accept HTTPRoute resources from a different namespace while placing the HTTPRoute in the same namespace as the service, depending on your management architecture.
In this scenario, the use of “ReferenceGrant” is unnecessary. This approach will be demonstrated in the section of SSL/TLS certificate setup using cert-manager.

Test the gateway controller#

To test the routes for the gateway controller, you can use curl to test the link and check for results:

Terminal window
curl -s http://${routeDomain}/default | grep "Default Service Page</div>"
curl -s http://${routeDomain}/external | grep "External Service Page</div>"

Output:

<div id="logo">Welcome to AKS Default Service Page</div>
<div id="logo">Welcome to AKS External Service Page</div>

Using SSL/TLS certificate in Gateway#

SSL/TLS certificates facilitate SSL/TLS encryption when a client communicates with a website. You can either bring your own certificates and store them in a Kubernetes Secret, integrate them with the Secrets Store CSI driver, or choose to use cert-manager to generate a publicly-trusted certificate.

Simple reference to certificate stored in secret#

The following manifest is a simple example of referencing a certificate from the secret “www-cert,” which is located in the “certificate” namespace:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
namespace: gateway-example
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: ${routeDomain}
allowedRoutes:
namespaces:
from: All
- name: https
port: 443
protocol: HTTPS
hostname: ${routeDomain}
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: www-cert
namespace: certificate

Remember to create a “ReferenceGrant” to enable cross-namespace references when the certificate secret is not in the same namespace as Gateway resource:

apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: www-cert-reference
namespace: certificate
spec:
to:
- group: ""
kind: Secret
name: www-cert
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: gateway-example

Using cert-manager to generate and configure publicly-trusted certificate#

If you need an application to generate and renew certificates for you, consider using cert-manager.
In this section, we will continue from the showcase above. Note that it is assumed that you have cert-manager installed in the cluster before proceeding.

  1. Create an namespace called “cert-issuer” for issuing certificate
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: cert-issuer
labels:
app.kubernetes.io/component: cert-issuer
EOF
  1. Define a Issuer
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: domain-cert-issuer
namespace: cert-issuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-acme-secret
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: the-sole-unique-gateway
namespace: default-service-and-gateway
kind: Gateway
EOF
TIP

For the list of well-known ACME servers, refer to: acme.sh wiki
Additionally, check out External Account Bindings when using a CA other than Let’s Encrypt.

NOTE

You do can use ClusterIssuer instead of an Issuer to make it cluster-scoped, depending on your architecture. In this case, a namespace-scoped Issuer will be used.

  1. Create Certificate resource to define request specifications
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: domain-cert
namespace: cert-issuer
spec:
issuerRef:
name: domain-cert-issuer
dnsNames:
- ${routeDomain}
privateKey:
algorithm: ECDSA
encoding: PKCS1
size: 256
secretName: domain-cert-secret
secretTemplate:
labels:
app.kubernetes.io/component: cert-issuer
EOF

The certificate will be stored in the secret “domain-cert-secret” within the “cert-issuer” namespace.

NOTE

After the Gateway references the Issuer (which will be done in a later step), a HTTPRoute resource will be automatically created as shown below:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cm-acme-http-solver-abcde
namespace: cert-issuer
spec:
parentRefs:
- name: the-sole-unique-gateway
namespace: default-service-and-gateway
kind: Gateway
hostnames:
- ${routeDomain}
rules:
- backendRefs:
- port: 8089
name: cm-acme-http-solver-abcde
weight: 1
matches:
- path:
type: Exact
value: /.well-known/acme-challenge/<TOKEN>

By default, Gateway only allows HTTPRoute resources from the namespace where it is located. In the next step, we will modify “allowedRoutes” to accept HTTPRoute resources from other namespaces, allowing the CA issuer to verify the request via HTTP.

  1. Grant permission to allow the Gateway to reference certificate Secret in different namespace
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: domain-cert-reference
namespace: cert-issuer
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default-service-and-gateway
to:
- group: ""
kind: Secret
name: domain-cert-secret
EOF
  1. Modify Gateway to use Issuer, referencing uncreated certificate Secret and extra HTTPRoute
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: the-sole-unique-gateway
namespace: default-service-and-gateway
labels:
app.kubernetes.io/component: gateway-api
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: ${routeDomain}
allowedRoutes:
namespaces:
from: Selector
selector:
matchExpressions:
- key: app.kubernetes.io/component
operator: In
values:
- gateway-api
- cert-issuer
- name: https
port: 443
protocol: HTTPS
hostname: ${routeDomain}
tls:
mode: Terminate
certificateRefs:
- name: domain-cert-secret
namespace: cert-issuer
kind: Secret
group: ""
infrastructure:
labels:
app.kubernetes.io/part-of: gateway-api
EOF

Now, Gateway will accept HTTPRoute resources from namespaces that have the label “app.kubernetes.io/component: gateway-api” or “app.kubernetes.io/component: cert-issuer”. This enables Gateway to route traffic to both cert-manager and applications.

TIP

You can also choose to allow HTTPRoute resources from different namespaces based on their names, as shown below:

allowedRoutes:
namespaces:
from: Selector
selector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- default-service-and-gateway
- cert-issuer
  1. Edit existing HTTPRoute to allow traffic to be routed under HTTPS communication
Terminal window
kubectl patch httproute service-one \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'
kubectl patch httproute service-two \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'
kubectl patch httproute service-one-static \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'

The Gateway manifest should look as follows:

10 collapsed lines
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
26 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /default
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-two
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: external-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
26 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /external
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-two
namespace: external-service
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one-static
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
9 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /static
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
  1. Wait for the certificate to be generated. Use the command below to check its existence:
Terminal window
kubectl get secret domain-cert-secret -n cert-issuer

It usually takes about 1 minute. Proceed to the next step if the certificate Secret is created.

  1. Test if traffic works under HTTPS communication
Terminal window
curl -s https://${routeDomain}/default | grep "Default Service Page</div>"
curl -s https://${routeDomain}/external | grep "External Service Page</div>"

Output:

<div id="logo">Welcome to AKS Default Service Page</div>
<div id="logo">Welcome to AKS External Service Page</div>

Using Secrets Store CSI Driver with your own certificates#

If you are deeply using managed service in Azure, then using Secrets Store CSI Driver will be a better choice.
In this section, we will continue using the same showcase from the previous section. Whether you skipped to this section or are reading from the start, I will provide the steps to reset the Gateway for consistency.
Note that it is assumed that you have relative add-ons installed on the cluster before proceeding.

  1. Use the following command to reset HTTPRoute and Gateway resources
NOTE

If you haven’t executed any commands in the section “Using cert-manager to generate and configure publicly-trusted certificate”, you can skip this step. However, even if you have executed it, there is no harm done.

Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
95 collapsed lines
metadata:
name: the-sole-unique-gateway
namespace: default-service-and-gateway
labels:
app.kubernetes.io/component: gateway-api
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: ${routeDomain}
infrastructure:
labels:
app.kubernetes.io/part-of: gateway-api
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /default
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-two
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: external-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /external
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-two
namespace: external-service
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one-static
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
hostnames:
- ${routeDomain}
rules:
- matches:
- path:
type: PathPrefix
value: /static
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
EOF
  1. Generate a SSL/TLS certificate in PFX format

a. Create and change to a temporary directory

Terminal window
tempDir=$(mktemp -d) && cd $tempDir

b. Generate a self-signed SSL/TLS certificate

Terminal window
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout private.key -out certificate.pem -days 30 -nodes \
-subj "/CN=${routeDomain}" -addext "subjectAltName=DNS:${routeDomain}"

c. Convert it to PFX format

Terminal window
openssl pkcs12 -export -in certificate.pem -passout pass: \
-inkey private.key -out certificate.pfx

The file certificate.pfx will be used in the following steps.

  1. Create an Azure Key Vault

a. Fill in an existing resource group name and a new Key Vault name

Terminal window
rG=
kv=

b. Create a new Key Vault

Terminal window
az keyvault create -n ${kv} -g ${rG} \
--enable-rbac-authorization true -o none

c. Get Key Vault ID and URI for further use

Terminal window
kvId=$(az resource list -n ${kv} -g ${rG} \
--resource-type Microsoft.KeyVault/vaults \
--query [0].id -o tsv)
kvUri=$(az keyvault show -n ${kv} -g ${rG} \
--query properties.vaultUri -o tsv)

d. Grant permision to yourself

Terminal window
userObjectId=$(az ad signed-in-user show --query id -o tsv)
az role assignment create --role "Key Vault Certificates Officer" \
--assignee-object-id ${userObjectId} -o none \
--scope ${kvId} --assignee-principal-type User
  1. Import the self-signed SSL/TLS certificate
Terminal window
az keyvault certificate import --vault-name ${kv} \
--name www-cert --file certificate.pfx -o none
  1. Connect Azure Key Vault with managed identity of Azure Key Vault Secrets Store CSI Driver
NOTE

In this section, managed identity method will be used for granting permissions. For more methods, refer to: Connect your Azure identity provider to the Azure Key Vault Secrets Store CSI Driver in Azure Kubernetes Service (AKS)

Terminal window
akvspObjectId=$(az aks show -n ${aks} -g ${rG} -o tsv \
--query addonProfiles.azureKeyvaultSecretsProvider.identity.objectId)
akvspClientId=$(az aks show -n ${aks} -g ${rG} -o tsv \
--query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId)
az role assignment create --role "Key Vault Certificate User" \
--assignee-object-id ${akvspObjectId} -o none \
--scope ${kvId} --assignee-principal-type ServicePrincipal
  1. Create a namespace for storing SSL/TLS certificate from key vault
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: certificate
labels:
app.kubernetes.io/component: certificate
EOF
  1. Deploy SecretProviderClass to reference SSL/TLS certificate

a. Set your Tenant ID

Terminal window
tenantId=

b. Deploy SecretProviderClass

Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: www-cert
namespace: certificate
spec:
provider: azure
secretObjects:
- secretName: www-cert-secret
type: kubernetes.io/tls
data:
- objectName: www-cert
key: tls.key
- objectName: www-cert
key: tls.crt
parameters:
useVMManagedIdentity: "true"
userAssignedIdentityID: ${akvspClientId}
keyvaultName: ${kv}
objects: |
array:
- |
objectName: www-cert
objectType: secret
tenantId: ${tenantId}
EOF
  1. Deploy dummy pods in the “certificate” namespace to trigger certificate synchronization
Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: www-cert-dummy
namespace: certificate
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: azure-secretprovider-dummy
template:
metadata:
labels:
app.kubernetes.io/name: azure-secretprovider-dummy
spec:
priorityClassName: system-node-critical
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/instance
operator: In
values:
- ngf
topologyKey: "kubernetes.io/hostname"
containers:
- name: dummy-pod
image: busybox
command: ["sleep", "infinity"]
volumeMounts:
- name: secrets-store-inline
readOnly: true
mountPath: /mnt/secrets-store
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: www-cert
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
EOF
NOTE

A secret can only be retrieved when the SecretProviderClass is referenced in volumes. SecretProviderClass must be in the same namespace as the Pod, and Secret will also be created in the same namespace as SecretProviderClass only.
This design necessitates the creation of a dummy Pod; otherwise, you can only place certificate Secrets within the “nginx-gateway” namespace.

Attaching volumes within the Nginx Fabric Gateway controller is not recommended, but if you choose to do so, use the following command:

Terminal window
helm upgrade --install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway \
--set nginx.pod.nodeSelector."kubernetes\.io/os"=linux \
--set nginxGateway.nodeSelector."kubernetes\.io/os"=linux \
--set certGenerator.nodeSelector."kubernetes\.io/os"=linux \
-f - <<EOF
nginxGateway:
extraVolumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: www-cert
extraVolumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
EOF
  1. Enable auto rotation in Azure Key Vault provider for Secrets Store CSI Driver
Terminal window
az aks addon update -n [your_aks_name] -g [aks_resource_group] -o none \
--addon azure-keyvault-secrets-provider --enable-secret-rotation

For introduction to auto rotation feature, refer to: Manage auto rotation

  1. Referencing SSL/TLS certificate and set-up ReferenceGrant

a. Reference SSL/TLS certificate in Gateway

Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: the-sole-unique-gateway
namespace: default-service-and-gateway
labels:
app.kubernetes.io/component: gateway-api
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
hostname: ${routeDomain}
- name: https
port: 443
protocol: HTTPS
hostname: ${routeDomain}
tls:
mode: Terminate
certificateRefs:
- name: www-cert-secret
namespace: certificate
kind: Secret
group: ""
infrastructure:
labels:
app.kubernetes.io/part-of: gateway-api
EOF

b. Grant permission to allow the Gateway to reference certificate Secret in different namespace

Terminal window
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: domain-cert-reference
namespace: certificate
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default-service-and-gateway
to:
- group: ""
kind: Secret
name: www-cert-secret
EOF

c. Editing existing HTTPRoute to allow traffic to be routed under HTTPS communication

Terminal window
kubectl patch httproute service-one \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'
kubectl patch httproute service-two \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'
kubectl patch httproute service-one-static \
-n default-service-and-gateway \
--type='json' \
-p='[{"op": "add", "path": "/spec/parentRefs/-", "value": {"name": "the-sole-unique-gateway", "sectionName": "https"}}]'

The Gateway manifest should look as follows:

10 collapsed lines
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
26 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /default
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-two
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: external-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
26 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /external
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: service-two
namespace: external-service
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: service-one-static
namespace: default-service-and-gateway
labels:
app.kubernetes.io/part-of: default-service
app.kubernetes.io/component: gateway-api
spec:
parentRefs:
- name: the-sole-unique-gateway
sectionName: http
- name: the-sole-unique-gateway
sectionName: https
hostnames:
- ${routeDomain}
9 collapsed lines
rules:
- matches:
- path:
type: PathPrefix
value: /static
backendRefs:
- name: service-one
namespace: default-service-and-gateway
port: 80
  1. Test if traffic works under HTTPS communication
Terminal window
curl -s -k https://${routeDomain}/default | grep "Default Service Page</div>"
curl -s -k https://${routeDomain}/external | grep "External Service Page</div>"

Output:

<div id="logo">Welcome to AKS Default Service Page</div>
<div id="logo">Welcome to AKS External Service Page</div>

Afterword#

This is the first article after my surgery and also the last article of the year.
Before writing this one, I had already written three drafts on different topics. However, one of them is pending a version release, another is stuck due to a bug, and for the last one, I don’t have a subscription to perform the lab. I could have released a new article earlier, but it never happened for various reasons.

This article also honors the archived document: Create an unmanaged ingress controller. This article is no longer being maintained, but it is a classic. To honor this article, I have imitated most of its style while trying to improve it. Most of the basic information can be found in this article.

NOTE

There was something wrong here when this article was initially published, but after clarification by the developer, I removed the incorrect content to maintain accuracy. The discussion history can be checked here.

Let’s set this topic aside for now. As the year comes to a close, let’s take a moment to say:

Happy New Year!

Wishing everyone all the best in the year ahead.

And my cat.

Create an unmanaged gateway controller in AKS with NGINX Gateway Fabric
https://blog.joeyc.dev/posts/aks-gateway-basic/
Author
Joey Chen
Published at
2025-12-31
License
CC BY-SA 4.0