Database connection strings with embedded passwords are one of the oldest and most preventable security risks in cloud applications. Yet I still see Password=SuperSecret123! sitting in app settings on projects that otherwise have mature security postures. Azure PostgreSQL Flexible Server with Entra ID authentication removes this entirely — no passwords, no rotation schedules, no accidental commits to git.
This is how we configure it at SandyTech on every Azure-hosted project that uses PostgreSQL.
App Service / AKS Pod
└── System-Assigned Managed Identity (or User-Assigned)
└── Entra ID Token (short-lived, auto-refreshed)
└── PostgreSQL Flexible Server (pg_aad_auth validates token)
└── DB Role mapped from Entra ID Group
The Managed Identity acquires a token from the IMDS endpoint (instance metadata service) at runtime. PostgreSQL validates this token against Entra ID using the pg_aad_auth extension. No password ever exists — there is nothing to leak, rotate, or store.
# Set an Entra ID admin at server creation time
az postgres flexible-server create \
--resource-group rg-myapp-prod \
--name pg-myapp-prod \
--location eastus \
--sku-name Standard_D2s_v3 \
--storage-size 64 \
--version 16 \
--active-directory-auth Enabled \
--password-auth Disabled # Disable password auth entirely (recommended)
# Or enable Entra ID auth on an existing server
az postgres flexible-server update \
--resource-group rg-myapp-prod \
--name pg-myapp-prod \
--active-directory-auth EnabledSetting --password-auth Disabled means no one can connect with a username/password — only Entra ID tokens are accepted. This is the zero-trust posture. If you have existing applications that need time to migrate, set both to Enabled temporarily.
Assign yourself (or a group) as the Entra ID admin:
az postgres flexible-server ad-admin create \
--resource-group rg-myapp-prod \
--server-name pg-myapp-prod \
--display-name "PG Admins Group" \
--object-id <entra-group-object-id>App Service (System-Assigned):
az webapp identity assign \
--resource-group rg-myapp-prod \
--name app-myapp-prod
# Outputs the principalId — save itAKS with Workload Identity (preferred for Kubernetes):
# Create user-assigned managed identity
az identity create \
--name id-myapp-prod \
--resource-group rg-myapp-prod
# Federate it with the Kubernetes service account
az identity federated-credential create \
--name fc-myapp-aks \
--identity-name id-myapp-prod \
--resource-group rg-myapp-prod \
--issuer <aks-oidc-issuer-url> \
--subject system:serviceaccount:myapp-ns:myapp-saConnect to the database as the Entra ID admin and create roles for your identities:
-- Connect as the Entra ID admin via psql with a token
-- (see connection string section below)
-- Create a role for the managed identity
-- The role name must exactly match the identity's display name or app registration name
SELECT * FROM pgaadauth_create_principal('app-myapp-prod', false, false);
-- Parameters: (display_name, is_azure_ad_only, is_superuser)
-- Grant schema permissions
GRANT CONNECT ON DATABASE myappdb TO "app-myapp-prod";
GRANT USAGE ON SCHEMA public TO "app-myapp-prod";
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "app-myapp-prod";
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "app-myapp-prod";Using Entra ID Groups for Role Management
For environments with multiple apps or multiple team members:
-- Create a principal for an Entra ID group
SELECT * FROM pgaadauth_create_principal('myapp-db-readwrite', false, false);
-- 'myapp-db-readwrite' is the Entra ID group display name
GRANT CONNECT ON DATABASE myappdb TO "myapp-db-readwrite";
GRANT USAGE ON SCHEMA public TO "myapp-db-readwrite";
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "myapp-db-readwrite";Now you manage database access by adding or removing members from the Entra ID group in Azure Portal — no SQL GRANT/REVOKE required when an employee joins or leaves.
The Azure.Identity library handles token acquisition transparently across environments — local dev (uses your az login session), CI (uses environment variables), and production (uses Managed Identity).
// Install: Azure.Identity, Npgsql.EntityFrameworkCore.PostgreSQL
using Azure.Identity;
using Azure.Core;
public static class DatabaseExtensions
{
public static IServiceCollection AddPostgresWithManagedIdentity(
this IServiceCollection services,
IConfiguration config)
{
services.AddDbContext<AppDbContext>(options =>
{
var connectionString = config["ConnectionStrings:Postgres"];
// connectionString = "Host=pg-myapp-prod.postgres.database.azure.com;
// Database=myappdb;Username=app-myapp-prod;Ssl Mode=Require;"
// Note: NO Password= in the connection string
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.UseAzureAdAuthentication(
new DefaultAzureCredential());
});
});
return services;
}
}DefaultAzureCredential tries a chain of credential sources in order:
AZURE_CLIENT_ID / AZURE_CLIENT_SECRET environment variables (CI/CD)az login) — local developmentThis means the same code works in every environment with zero changes.
Connection string in appsettings.json:
{
"ConnectionStrings": {
"Postgres": "Host=pg-myapp-prod.postgres.database.azure.com;Database=myappdb;Username=app-myapp-prod;Ssl Mode=Require;Trust Server Certificate=false"
}
}There is no Password field. If someone extracts this connection string, they cannot connect without a valid Entra ID token bound to the identity.
Developers on the team need access too. Add each developer's Entra ID account as a principal:
SELECT * FROM pgaadauth_create_principal('developer@yourcompany.com', false, false);
GRANT myapp-db-readwrite TO "developer@yourcompany.com";Or better — add them to the myapp-db-readwrite Entra ID group and let PostgreSQL resolve group membership at connection time.
Connecting via psql locally:
# Get a token via Azure CLI
TOKEN=$(az account get-access-token \
--resource-type oss-rdbms \
--query accessToken -o tsv)
PGPASSWORD=$TOKEN psql \
"host=pg-myapp-prod.postgres.database.azure.com \
dbname=myappdb \
user=developer@yourcompany.com \
sslmode=require"Use a shell alias or a script to wrap this — the token is short-lived (60 minutes) so developers will need to refresh it periodically.
"pg_aad_auth extension not found" The extension is pre-installed on Flexible Server but not auto-loaded. Add it via server parameters:
az postgres flexible-server parameter set \
--resource-group rg-myapp-prod \
--server-name pg-myapp-prod \
--name shared_preload_libraries \
--value "pg_aad_auth"A server restart is required.
"role does not exist" error
The role name must exactly match the Managed Identity's display name (not its client ID). Check with az identity show --name id-myapp-prod --resource-group rg-myapp-prod --query name.
Token expiry in long-running apps
Npgsql with UseAzureAdAuthentication handles token refresh automatically at connection open. For raw ADO.NET or connection pool edge cases, implement a PeriodicTimer to refresh the connection pool every 45 minutes.
On a recent SandyTech project, we went through a security audit. The auditor specifically asked about database credential rotation. The answer was "there are no credentials to rotate" — that is a conversation-ender in the best possible way. Managed Identity authentication is not just a security improvement; it eliminates an entire category of operational burden.