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:
| Extensions | Node Size | Min Nodes | Max Nodes |
|---|---|---|---|
| < 5,000 | Standard_D4s_v5 (4 vCPU, 16 GiB) | 3 | 6 |
| 5,000--25,000 | Standard_D8s_v5 (8 vCPU, 32 GiB) | 3 | 10 |
| 25,000--50,000 | Standard_D16s_v5 (16 vCPU, 64 GiB) | 5 | 15 |
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:
| Record | Type | Value | Purpose |
|---|---|---|---|
surfacesec.yourdomain.com | A | $INGRESS_IP | Admin dashboard |
api.surfacesec.yourdomain.com | A | $ENVOY_IP | Extension 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
- Log in to the admin dashboard at
https://surfacesec.yourdomain.com - Navigate to Onboarding
- Set the Extension API Endpoint to
https://api.surfacesec.yourdomain.com - Generate enrollment tokens for new devices
- 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)
| Extensions | AKS Nodes | API Pods | Ingestion Pods | Envoy Pods | PG Tier |
|---|---|---|---|---|---|
| 1,000 | 3 x D4s_v5 | 3 | 3 | 2 | D4ds_v5 |
| 5,000 | 4 x D4s_v5 | 5 | 5 | 3 | D8ds_v5 |
| 10,000 | 5 x D8s_v5 | 10 | 8 | 4 | D8ds_v5 |
| 25,000 | 7 x D8s_v5 | 15 | 12 | 6 | D16ds_v5 |
| 50,000 | 10 x D16s_v5 | 20 | 15 | 10 | D32ds_v5 |