Skip to main content

Azure AKS Deployment

Azure-specific instructions for deploying Surface Security on Azure Kubernetes Service (AKS) with internet-facing telemetry ingestion from remote browser extensions.

Architecture Overview

+--------------------------------------+
| Azure Front Door / DNS |
| surfacesec.yourdomain.com |
+----------+---------------+-----------+
| |
HTTPS (admin) | | mTLS (extensions)
| |
+----------v---+ +------v-----------+
| NGINX Ingress| | Azure LB (L4) |
| (TLS term) | | -> Envoy Service |
+------+-------+ +------+-----------+
| |
+-------------v------+ +--------v----------+
| Admin API / SCIM | | Envoy (mTLS term) |
| Frontend Dashboard | | Client cert valid |
+-------------+------+ +--------+----------+
| |
+------v--------------------v----------+
| surfacesec-api Service |
+------------------+-------------------+
|
+------------+------------+------------+-------------+
| | | | |
+-----v-----+ +---v----+ +----v----+ +----v---+ +--------v------+
| PostgreSQL | |ClickHs | |Redpanda | | MinIO | | Ingestion |
| Flex Server| | (pod) | | (pod) | | (blob) | | Workers |
+-----------+ +--------+ +---------+ +--------+ +---------------+

Two separate entry points:

  • NGINX Ingress (HTTPS): Admin dashboard, SCIM provisioning, enrollment -- standard TLS
  • Envoy LoadBalancer (mTLS): Extension API telemetry -- mutual TLS with per-device client certificates

Step 1: Provision Azure Resources

Create Resource Group

RESOURCE_GROUP="surfacesec-rg"
LOCATION="centralus"

az group create --name $RESOURCE_GROUP --location $LOCATION

Create AKS Cluster

AKS_CLUSTER="surfacesec-aks"

az aks create \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--node-count 3 \
--node-vm-size Standard_D4s_v5 \
--enable-managed-identity \
--network-plugin azure \
--network-policy calico \
--generate-ssh-keys \
--zones 1 2 3 \
--enable-cluster-autoscaler \
--min-count 3 \
--max-count 10 \
--tier standard

# Get credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER

Node sizing guidance:

ExtensionsNode SizeMin NodesMax Nodes
< 5,000Standard_D4s_v5 (4 vCPU, 16 GiB)36
5,000--25,000Standard_D8s_v5 (8 vCPU, 32 GiB)310
25,000--50,000Standard_D16s_v5 (16 vCPU, 64 GiB)515

Create Azure Container Registry

ACR_NAME="surfacesecacr"

az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Standard

# Attach ACR to AKS (so AKS can pull images)
az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--attach-acr $ACR_NAME

Create Azure Database for PostgreSQL Flexible Server

PG_SERVER="surfacesec-pg"
PG_ADMIN_USER="surfacesec_admin"
PG_ADMIN_PASSWORD="$(openssl rand -base64 32)"

az postgres flexible-server create \
--resource-group $RESOURCE_GROUP \
--name $PG_SERVER \
--location $LOCATION \
--admin-user $PG_ADMIN_USER \
--admin-password "$PG_ADMIN_PASSWORD" \
--sku-name Standard_D4ds_v5 \
--storage-size 128 \
--version 16 \
--tier GeneralPurpose \
--high-availability ZoneRedundant

# Create the surfacesec database
az postgres flexible-server db create \
--resource-group $RESOURCE_GROUP \
--server-name $PG_SERVER \
--database-name surfacesec

Recommended: Use VNet integration or Private Endpoints for PostgreSQL instead of open firewall rules in production.

Create Azure Blob Storage (replaces MinIO)

STORAGE_ACCOUNT="surfacesecstorage"

az storage account create \
--resource-group $RESOURCE_GROUP \
--name $STORAGE_ACCOUNT \
--location $LOCATION \
--sku Standard_ZRS \
--kind StorageV2

# Create screenshot container
az storage container create \
--account-name $STORAGE_ACCOUNT \
--name surfacesec-screenshots

Step 2: Install NGINX Ingress Controller

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"="/healthz"

Step 3: Create Namespace and Secrets

kubectl create namespace surfacesec

# PostgreSQL credentials
kubectl create secret generic surfacesec-db-credentials \
--namespace=surfacesec \
--from-literal=host="$PG_SERVER.postgres.database.azure.com" \
--from-literal=database="surfacesec" \
--from-literal=username="$PG_ADMIN_USER" \
--from-literal=password="$PG_ADMIN_PASSWORD"

# ClickHouse credentials (ClickHouse runs as a pod in the cluster)
CH_PASSWORD="$(openssl rand -base64 24)"
kubectl create secret generic surfacesec-clickhouse-credentials \
--namespace=surfacesec \
--from-literal=username="surfacesec" \
--from-literal=password="$CH_PASSWORD"

# Blob Storage credentials
STORAGE_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP \
--account-name $STORAGE_ACCOUNT --query "[0].value" -o tsv)
kubectl create secret generic surfacesec-minio-credentials \
--namespace=surfacesec \
--from-literal=endpoint="$STORAGE_ACCOUNT.blob.core.windows.net" \
--from-literal=access-key="$STORAGE_ACCOUNT" \
--from-literal=secret-key="$STORAGE_KEY"

# Surface Security license
kubectl create secret generic surfacesec-surface-license \
--namespace=surfacesec \
--from-literal=license-key="YOUR_SIGNED_LICENSE_KEY"

Step 4: Build and Push Container Images

# Login to ACR
az acr login --name $ACR_NAME

# Build and push
docker build -t $ACR_NAME.azurecr.io/surfacesec/api:1.0.0 -f backend/Dockerfile backend/
docker build -t $ACR_NAME.azurecr.io/surfacesec/ingestion:1.0.0 -f backend/Dockerfile.ingestion backend/
docker build -t $ACR_NAME.azurecr.io/surfacesec/frontend:1.0.0 -f frontend/Dockerfile frontend/

docker push $ACR_NAME.azurecr.io/surfacesec/api:1.0.0
docker push $ACR_NAME.azurecr.io/surfacesec/ingestion:1.0.0
docker push $ACR_NAME.azurecr.io/surfacesec/frontend:1.0.0

Step 5: Deploy All Services

Apply manifests in dependency order:

# ConfigMap and Secrets
kubectl apply -f infra/k8s/base/configmap.yaml
kubectl apply -f infra/k8s/base/secrets.yaml

# Data layer
kubectl apply -f infra/k8s/base/redpanda-statefulset.yaml
kubectl wait --for=condition=ready pod/redpanda-0 -n surfacesec --timeout=120s

# Application services
kubectl apply -f infra/k8s/base/api-deployment.yaml
kubectl apply -f infra/k8s/base/ingestion-deployment.yaml
kubectl apply -f infra/k8s/base/frontend-deployment.yaml

# Envoy mTLS gateway
kubectl apply -f infra/k8s/base/envoy-deployment.yaml

# NGINX Ingress
kubectl apply -f infra/k8s/base/ingress.yaml

Step 6: Configure DNS

After deployment, get the external IPs:

# NGINX Ingress IP (admin dashboard)
INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Dashboard IP: $INGRESS_IP"

# Envoy mTLS IP (extension API)
ENVOY_IP=$(kubectl get svc -n surfacesec surfacesec-envoy \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Extension API IP: $ENVOY_IP"

Create DNS records:

RecordTypeValuePurpose
surfacesec.yourdomain.comA$INGRESS_IPAdmin dashboard
api.surfacesec.yourdomain.comA$ENVOY_IPExtension mTLS API

Important: The extension's API endpoint must be configured to use the Envoy IP/hostname (e.g., https://api.surfacesec.yourdomain.com), not the ingress hostname. This ensures extensions connect via mTLS.

Step 7: Verify the Deployment

# All pods healthy
kubectl get pods -n surfacesec

# License verified
kubectl logs -n surfacesec -l component=api --tail=5 | grep "license verified"

# Health endpoints
curl https://surfacesec.yourdomain.com/health

# Envoy mTLS endpoint (requires client certificate)
curl --cert device.crt --key device.key --cacert ca.crt \
https://api.surfacesec.yourdomain.com/health

# HPA status
kubectl get hpa -n surfacesec

Step 8: Configure Extension Enrollment

  1. Log in to the admin dashboard at https://surfacesec.yourdomain.com
  2. Navigate to Onboarding
  3. Set the Extension API Endpoint to https://api.surfacesec.yourdomain.com
  4. Generate enrollment tokens for new devices
  5. Deploy the extension to browsers (see Extension Deployment)

Azure-Specific Recommendations

Managed Identity for Pod Authentication

Instead of storing Azure credentials in Kubernetes secrets, use Workload Identity:

az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--enable-oidc-issuer \
--enable-workload-identity

Azure Monitor Integration

# Enable Container Insights
az aks enable-addons \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--addons monitoring

# Enable Prometheus metrics
az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--enable-azure-monitor-metrics

Network Security

Restrict Envoy access to only allow HTTPS (443) from the internet:

az network nsg rule create \
--resource-group $RESOURCE_GROUP \
--nsg-name surfacesec-nsg \
--name allow-envoy-mtls \
--priority 100 \
--direction Inbound \
--access Allow \
--protocol Tcp \
--destination-port-ranges 443

Scaling Reference (Azure AKS)

ExtensionsAKS NodesAPI PodsIngestion PodsEnvoy PodsPG Tier
1,0003 x D4s_v5332D4ds_v5
5,0004 x D4s_v5553D8ds_v5
10,0005 x D8s_v51084D8ds_v5
25,0007 x D8s_v515126D16ds_v5
50,00010 x D16s_v5201510D32ds_v5