Microsoft Entra authentication for Azure PostgreSQL
Figaf Tool can authenticate to Azure Database for PostgreSQL flexible server with a Microsoft Entra ID access token instead of a database password, so no database credential has to be stored in the deployment. Access is granted to an Azure identity and revoked centrally in Entra ID.
Only the source of the password changes - the JDBC driver, the connection pool and Liquibase migrations behave exactly as before. The mode is opt-in: deployments that keep using a password, other database systems, SAP BTP and Figaf Cloud aren’t affected.
| The feature requires 2606.4 or later in the 2606 line, or 2609 or later. |
Steps 1-5 are Azure-side and need Entra administrator rights on the server. Steps 6-8 are done on the Figaf Tool instance.
1. Prerequisites
-
irt.db.type=postgresql, and the database is Azure Database for PostgreSQL flexible server. Single Server, H2, Oracle, MS SQL Server and DB2 can’t use this. -
PG_TRGMallow-listed in theazure.extensionsserver parameter (step 5) - needed even if the extension itself isn’t wanted. -
PostgreSQL JDBC driver 42.6.0 or later. The Docker image ships a current driver, so there is nothing to do there. On a standalone deployment the driver is supplied by the installation itself, so check the jar in the
libsfolder - see step 6. -
Network path to Entra ID. With VNet integration, an outbound rule to the
AzureActiveDirectoryservice tag is required and custom DNS must resolvelogin.microsoftonline.com.
2. Choosing an identity
Every option needs the same two settings - irt.db.auth-mode=azure-entra and irt.db.username set to the PostgreSQL role name. What differs is only where the token comes from:
| Identity | Use when | Additional configuration |
|---|---|---|
User-assigned managed identity |
Figaf Tool runs on an Azure VM, App Service or Container Apps |
|
System-assigned managed identity |
As above |
None - leave the client id unset |
AKS with Microsoft Entra Workload ID |
Figaf Tool runs in Azure Kubernetes Service |
None - the client id must stay unset, otherwise token acquisition is restricted to managed identity and fails |
Service principal with a client secret |
Figaf Tool runs outside Azure against an Azure database |
|
Entra user ( |
Administration and troubleshooting only, never for the application |
None |
Both managed identity types work equally well. Keep in mind that the database role is bound to the identity’s object id, not to its name, and a system-assigned identity lives and dies with the resource it belongs to: rebuilding the VM produces a new object id and silently breaks the database role even though every name stays the same, so the role has to be linked again (step 4). A user-assigned identity survives host replacement.
A service principal is a fallback rather than an equal option - it reintroduces a stored secret with an expiry date. Use it only where no managed identity is available.
3. Step 1: Enable Entra authentication on the server
az postgres flexible-server update -g <rg> -n <srv> --microsoft-entra-auth Enabled --password-auth Enabled
Keep both methods enabled for the whole migration. That’s a fully supported state and it’s what makes rollback possible. The flag is --microsoft-entra-auth on the current Azure CLI and --active-directory-auth on the LTS channel.
| This restarts the server and drops all connections - it isn’t a dynamic parameter change, so schedule a maintenance window. |
From the portal: open the server → Settings → Security → under Authentication method choose PostgreSQL and Microsoft Entra authentication → Save.
4. Step 2: Assign a Microsoft Entra administrator
Only an Entra administrator can create or link Entra database roles, so this must come before step 4. Add yourself here.
az postgres flexible-server microsoft-entra-admin create \
-g <rg> -s <srv> -u <[email protected]> -i <your-object-id> -t User
On the LTS CLI the command group is ad-admin. Use -t ServicePrincipal for an app registration or managed identity.
From the portal: the same Security page → Microsoft Entra authentication → Add Microsoft Entra Admins → pick the user or group → Save. The picker resolves the object id for you.
5. Step 3: Create the managed identity
az identity create -g <rg> -n <uami>
az vm identity assign -g <rg> -n <vm> --identities <uami-resource-id>
az identity show -g <rg> -n <uami> --query "{clientId:clientId, principalId:principalId}"
The last command returns the two values you need:
| CLI | Portal field | Used for |
|---|---|---|
|
Client ID |
|
|
Object ID |
the identity link on the database role in step 4 |
|
These are two different values, and mixing them up produces a role that can never authenticate - the resulting error mentions neither field. For an app registration, the object id must come from Enterprise applications (the service principal), not from App registrations (the application object). This is the most common setup error. |
In the portal, both ids are shown on the Overview page of the identity, and the identity is attached to the host under its Security → Identity page.
6. Step 4: Link the PostgreSQL role to the identity
Connect to the postgres database as the Entra administrator - the pgaadauth functions exist only there, and elsewhere they fail with a confusing No function matches the given name error:
export PGPASSWORD=$(az account get-access-token --resource-type oss-rdbms --query "[accessToken]" -o tsv)
psql "host=<srv>.postgres.database.azure.com user=<[email protected]> dbname=postgres sslmode=require"
The token goes through PGPASSWORD because it’s longer than psql’s interactive prompt accepts.
Existing password-based deployment (the common case). The database, the schema and the role Figaf Tool connects as already exist, and only the credential is changing. Attach the Entra identity to that existing role:
SECURITY LABEL FOR "pgaadauth" ON ROLE "<existing_role>" IS 'aadauth,oid=<principalId>,type=service';
Ownership, grants, default privileges and search_path stay as they are, irt.db.username keeps its current value, and rollback is removing the label. Nothing else is needed on the database side.
Don’t create a new role for an existing deployment. That gives you a second role that owns nothing, while the role holding the data still can’t authenticate. Ownership transfer isn’t needed either, and ALTER DATABASE … OWNER TO can be refused outright because the superuser role is Microsoft-managed.
|
New installation. Create the role, then the database, schema and privileges. Figaf Tool doesn’t create its database or schema, and an Entra role is an ordinary non-superuser, so both must exist beforehand:
SELECT * FROM pgaadauth_create_principal_with_oid('figaf_app', '<principalId>', 'service', false, false);
CREATE DATABASE "<db>";
\c <db>
CREATE SCHEMA IF NOT EXISTS irt;
CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA irt;
GRANT CONNECT ON DATABASE "<db>" TO "figaf_app";
GRANT USAGE, CREATE ON SCHEMA irt TO "figaf_app";
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA irt TO "figaf_app";
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA irt TO "figaf_app";
ALTER DEFAULT PRIVILEGES IN SCHEMA irt GRANT ALL ON TABLES TO "figaf_app";
ALTER DEFAULT PRIVILEGES IN SCHEMA irt GRANT ALL ON SEQUENCES TO "figaf_app";
figaf_app is a free choice of role name - it’s what goes into irt.db.username. The two false arguments are isAdmin and isMfa; isMfa must stay false for a machine identity, whose token carries no multifactor claim. The ALTER DEFAULT PRIVILEGES lines matter too: without them the next upgrade creates objects the application can’t read.
Either way, confirm the binding registered with SELECT * FROM pgaadauth_list_principals(false);.
7. Step 5: Prepare the PostgreSQL extensions
Figaf Tool uses pg_trgm for text search indexes, and optionally vector for embedding features. Azure refuses to create any extension that isn’t listed in the azure.extensions server parameter. The parameter is a list and setting it replaces the whole value, so read it first:
az postgres flexible-server parameter show -g <rg> -s <srv> --name azure.extensions --query value -o tsv
az postgres flexible-server parameter set -g <rg> -s <srv> --name azure.extensions --value PG_TRGM
Add VECTOR as well if the embedding features are wanted. The portal equivalent (server → Server parameters → azure.extensions) is safer, because it shows the current selection as checkboxes and submits the full set. No restart is needed.
Allow-list PG_TRGM even on a deployment that doesn’t want the extension. The migration that creates it runs regardless, and Azure refuses it with an error class that HikariCP treats as fatal, which drops the Liquibase connection underneath the running migration.
|
pg_trgm also has to be resolvable from the application’s schema. Figaf Tool pins search_path to its own schema, so an extension created without a SCHEMA clause lands in public where the application can’t resolve its operators, and start-up fails with operator class "gin_trgm_ops" does not exist, reported as a misleading Unable to start web server. To see where it already lives, from the application database:
SELECT e.extname, n.nspname AS schema
FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace
WHERE e.extname IN ('pg_trgm', 'vector');
Set irt.db.append-pg-trgm-schema-to-search-path=true in step 7 for any Azure deployment: it’s mandatory if the extension sits in public, and otherwise costs one catalog query at start-up.
8. Step 6: Add the Azure client libraries
Figaf Tool doesn’t bundle the Azure libraries. They are supplied the same way the PostgreSQL driver already is: as jar files in the folder passed to the JVM with -Dloader.path. The files are only needed by a deployment that uses Entra authentication - with irt.db.auth-mode absent or set to password no Azure code is loaded, so upgrading a password-based deployment requires nothing and after a rollback the jars can stay in place.
On a standalone deployment, check the PostgreSQL driver first. The authenticationPluginClassName parameter was introduced in pgjdbc 42.6.0, and earlier drivers ignore unknown connection parameters silently:
unzip -p libs/postgresql*.jar META-INF/MANIFEST.MF | grep -i Implementation-Version
The jar is often renamed to a bare postgresql.jar, so the file name proves nothing - read the manifest. When replacing it, move the old jar out of the folder rather than leaving both, because every jar in a loader.path directory joins the classpath.
Then place these eight jars in the same folder. All are published by Microsoft on Maven Central and MIT licensed:
| Artifact | Version | Download |
|---|---|---|
|
1.2.9 |
|
|
1.18.4 |
|
|
1.58.1 |
|
|
1.5.1 |
|
|
1.2.1 |
|
|
1.1.5 |
|
|
1.23.1 |
|
|
1.3.0 |
Don’t substitute other versions - this set is what the shipped application was verified against. If Maven Central isn’t reachable from the host, pull the same coordinates through your own artifact proxy or ask Figaf support for the eight files as a bundle.
In the Docker distribution the entrypoint already passes -Dloader.path=/libs, so the jars are added by mapping a volume with them to /libs, for example -v /opt/figaf/libs:/libs. Mounting a volume there hides the driver baked into the image, so the mapped folder also needs its own postgresql.jar of 42.6.0 or later next to the eight Azure jars.
Don’t add any other jar to that folder. loader.path takes precedence over the libraries packaged inside the application, so adding slf4j-api, Jackson, reactor-core or Netty there would downgrade the running application - Figaf Tool already ships all of them at an equal or newer version.
|
A managed identity (and AKS workload identity) takes its token from the local instance metadata endpoint 169.254.169.254, which Azure doesn’t support reaching through an HTTP proxy. If the host has one configured, exclude that address for the Figaf Tool process.
9. Step 7: Configure Figaf Tool
These are ordinary application properties - pass them on the java command line or put them into config/application.yml beside the jar:
| Property | Default | Meaning |
|---|---|---|
|
|
Set to |
|
- |
The PostgreSQL role name bound to the Entra identity, not the Azure resource name. Case-sensitive. |
|
- |
Ignored when |
|
empty |
Client id of the user-assigned managed identity. Setting it restricts token acquisition to managed identity alone, so leave it empty for a system-assigned identity, AKS Workload ID or a service principal. |
|
not set |
Set to |
|
|
Set to |
A minimal config/application.yml for a user-assigned managed identity:
irt:
db:
type: postgresql
host: <server>.postgres.database.azure.com
port: 5432
name: <database>
schema: irt
username: <postgresql-role-name>
auth-mode: azure-entra
sslmode: require
append-pg-trgm-schema-to-search-path: true
azure:
managed-identity-client-id: <client-id>
For a system-assigned identity, AKS Workload ID or a service principal, remove the whole azure: block - everything else stays identical. For a service principal, additionally set AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET as environment variables of the application process. Don’t leave those three set in an environment that should use a managed identity: they take precedence over it, and the deployment then works only until the secret expires.
In the Docker distribution the same configuration is passed as environment variables, with IRT_DB_PASSWORD left unset:
IRT_DB_AUTH_MODE=azure-entra
IRT_DB_AZURE_MANAGED_IDENTITY_CLIENT_ID=<client-id>
IRT_DB_APPEND_PG_TRGM_SCHEMA_TO_SEARCH_PATH=true
IRT_DB_USER=<postgresql-role-name>
IRT_DB_SSLMODE=require
In Entra mode the container skips the two psql statements it normally runs to create the database and schema, because those need a static password. It logs Skipping database and schema creation because IRT_DB_AUTH_MODE is azure-entra, both must already exist - create them as described in step 4. The BTP image isn’t covered: it takes its credentials from the Cloud Foundry service binding and there is no managed identity on that platform.
10. Step 8: Verify
A healthy start-up logs, in order:
Microsoft Entra database authentication is enabled, ...
Ossrdbms scope set to https://ossrdbms-aad.database.windows.net/.default
No PostgreSQL extension schemas need to be appended to search_path
Database type: postgresql, url: jdbc:postgresql://...&authenticationPluginClassName=..., user: figaf_app
HikariPool-1 - Added connection
Liquibase: Update has been successful
The first line ends with managed identity client id: <client-id> when the property is set, and with the default Azure credential chain will be used when it isn’t. Seeing the client-id form where you intended a system-assigned identity means the property is still set somewhere.
Then confirm from a SQL client that the role is bound to the intended object id (SELECT * FROM pgaadauth_list_principals(false);) and that the application connects as the expected role (SELECT current_user;).
|
Don’t confuse it with the two benign lookalikes, |
11. Optional: disable password authentication
Up to this point rollback is reverting one property and restarting, and leaving both authentication methods enabled is fully supported. The only thing Entra-only buys is that a leaked or stale password becomes useless:
az postgres flexible-server update -g <rg> -n <srv> --password-auth Disabled
|
This is server-wide, not per role. Password login disappears for every role at once, including the server administrator account, and only roles carrying an Entra binding can connect afterwards. Before disabling it, account for everything else that logs in with a password - |
Then delete the old password from wherever the deployment stored it.
12. Troubleshooting
Enable Azure SDK logging first. Without it the Azure library is silent, and the PostgreSQL driver only ever reports a missing or malformed password without being able to say why. Three independent settings are needed, and any one of them left out produces complete silence:
-
AZURE_LOG_LEVEL=verboseas an environment variable of the application process (-DAZURE_LOG_LEVEL=verboseworks too). -
logging.level.com.azure: DEBUGinconfig/application.yml. -
irt.logging.stdout-filter-level: DEBUGinconfig/application.yml, only needed to read the output on the console -logs/app.logalready has the lines once the first two are set.
The Azure Identity library then logs every credential it tries and stops at the first success (Azure Identity ⇒ Attempted credential ManagedIdentityCredential returns a token). A complete absence of Azure Identity ⇒ lines means the library was never invoked at all, so suspect the driver version first and then a missing jar. Turn the three settings off once the deployment is healthy.
| Error | Cause and fix |
|---|---|
|
The Azure jars are missing from the classpath. Confirm all eight are present and that |
|
The start-up guard found a driver too old to honour |
|
An old driver that the guard couldn’t read: it ignored |
|
The object id on the role doesn’t match the token - usually the identity was recreated, or the application object id was used instead of the service principal’s. Compare |
|
The generic failure: Entra authentication not enabled, password authentication disabled while a password was sent, the role missing, or a role name case mismatch. |
|
No managed identity on this host, or the metadata endpoint is unreachable - check that the identity is assigned and that no HTTP proxy intercepts |
|
The extension isn’t in |
|
|
|
Not connected to the |
|
Connecting through PgBouncer with a token too large for its buffer, which affects human users with many group memberships. Connect directly on port 5432; workload identities aren’t affected. |
13. Good to know when operating it
-
A token is checked only when a connection is opened, so a long Liquibase migration is safe. The other side of this: revoking an identity’s access doesn’t close existing connections - they persist until the pool retires them, within 30 minutes by default. Restart the application when immediate revocation matters.
-
No pool tuning is required. The pool obtains a current token for every new physical connection, and the Azure library caches tokens and refreshes them shortly before expiry.
-
Promoting a read replica doesn’t inherit the primary’s Entra configuration - the authentication mode is set per server, so configure it explicitly on the promoted one. Zone-redundant failover needs no action.
-
Any process on the host can request a token from the local metadata endpoint and connect as the application’s identity, so don’t co-locate untrusted workloads with Figaf Tool.