← Back to Projects

Hardening a Developer Portal on AKS

Taking a proof-of-concept portal to production on shared AKS clusters with workload identity, private endpoints and SSO

Platform: AKS (shared clusters)
Identity: Workload Identity + Entra SSO
Data: PostgreSQL + Blob (private)
Role: Lead engineer

Overview

The developer portal began as a single-replica proof of concept with guest login and nowhere durable to keep its state. I rebuilt how it runs so it could serve the whole engineering organisation. It now runs on shared multi-tenant AKS clusters with no stored credentials, private-only data paths, group-based SSO and its own isolated node pool.

Starting Point vs. Target

AreaProof of conceptProduction
AuthenticationGuest login, so everyone was signed in automaticallyEntra ID SSO gated on security groups
StateIn-memory or localManaged PostgreSQL, zone-redundant in production
Docs storageNoneBlob Storage behind a private endpoint
SecretsKubernetes secrets created by handKey Vault via the CSI driver and workload identity
SchedulingAnywhere on the clusterDedicated tainted node pool
Replicas13 (after fixing a connection ceiling)

Architecture

UsersSSO via Entra IDInternal ingressNGINXPortal podsdedicated node poolWorkload identityfederated SAKey Vaultprivate endpointPostgreSQLprivate endpointBlob (docs)private endpointPrivate DNSauthoritative zone
Portal runtime: every data path is private and every credential is federated

Implementation

Pinning pods to their own pool

values.yaml
# values.yaml (illustrative)
nodeSelector:
  workload: developer-portal
tolerations:
  - key: dedicated
    operator: Equal
    value: developer-portal
    effect: NoSchedule
serviceAccount:
  annotations:
    azure.workload.identity/client-id: <managed-identity-client-id>

SSO that respects nested groups

The groups claim only lists direct memberships in the configuration we used, so people who belonged through a nested group were locked out. I added a server-side fallback that calls Microsoft Graph checkMemberGroups to resolve transitive membership authoritatively. The scaffolder is gated on a separate approvers group.

Sensible session lifetime

The default refresh-token cookie lasted more than a year. I capped sessions at a working day.

Troubleshooting: Six Issues Worth Knowing

SymptomRoot causeFix
Secret mount fails with no such hostThe private endpoint's DNS record wasn't in the zone the corporate resolver treats as authoritativeRegister the record in the authoritative zone through an aliased Terraform provider
Service and endpoints exist, but curl returns 000Named targetPort, but the chart doesn't name the container portUse a numeric targetPort
Per-environment config ignoredThe chart silently drops args, so the image CMD always runsOne config file with ${VAR} placeholders, filled from env vars
remaining connection slots are reservedAbout 30 DB connections per pod against a low default max_connectionsBigger SKU and explicit max_connections per environment
Deploy succeeds, nothing changesThe mutable :main tag left the pod spec unchangedTag images with the commit SHA
Deploy fails patching the namespaceThe deploy identity only had namespace-scoped adminTerraform owns namespaces, and the deploy skips creating them

Key Outcomes

Conclusion

Most of the effort was at the edges: DNS, chart behaviour, connection limits and identity claims. Writing each one down turned a hard deployment into a repeatable pattern for the next team.

Technologies Used

AKSHelmTerraformAzure Workload IdentityKey Vault CSI DriverIstioNGINX IngressPostgreSQL Flexible ServerBlob StoragePrivate EndpointsPrivate DNSEntra IDMicrosoft Graph

About this case study: this write-up is drawn from my hands-on platform engineering experience. Organisation names, system names, figures and timelines have been changed or generalised, and all code, configuration and queries are illustrative examples written for this page.

← Internal Developer Platform with Golden PathsDebugging a Managed CoreDNS Regression →
← Back to Projects Get in Touch