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_TRGM allow-listed in the azure.extensions server 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 libs folder - see step 6.

  • Network path to Entra ID. With VNet integration, an outbound rule to the AzureActiveDirectory service tag is required and custom DNS must resolve login.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

irt.db.azure.managed-identity-client-id=<client id>

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

AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET as environment variables; client id unset

Entra user (az login)

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

clientId

Client ID

irt.db.azure.managed-identity-client-id in Figaf Tool

principalId

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.

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

com.azure:azure-identity-extensions

1.2.9

jar

com.azure:azure-identity

1.18.4

jar

com.azure:azure-core

1.58.1

jar

com.azure:azure-json

1.5.1

jar

com.azure:azure-xml

1.2.1

jar

com.azure:azure-core-http-jdk-httpclient

1.1.5

jar

com.microsoft.azure:msal4j

1.23.1

jar

com.microsoft.azure:msal4j-persistence-extension

1.3.0

jar

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

irt.db.auth-mode

password

Set to azure-entra to authenticate with Entra tokens. Any other value keeps username and password authentication.

irt.db.username

-

The PostgreSQL role name bound to the Entra identity, not the Azure resource name. Case-sensitive.

irt.db.password

-

Ignored when auth-mode is azure-entra. Remove it.

irt.db.azure.managed-identity-client-id

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.

irt.db.sslmode

not set

Set to require at minimum. The token travels as the password, so encryption in transit is essential.

irt.db.append-pg-trgm-schema-to-search-path

false

Set to true unless pg_trgm lives in the application’s own schema - see step 5.

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;).

Unable to discover PostgreSQL extension schemas, search_path will not include them is an error, not a warning. Figaf Tool opens a short-lived connection at start-up to locate extension schemas, and this message means it couldn’t authenticate - the application continues and then silently loses trigram and vector index support.

Don’t confuse it with the two benign lookalikes, gin_trgm_ops not found no pg_trgm schema added to search_path and pgvector not found no vector schema added to search_path. Those mean the probe connected and the extension simply isn’t installed.

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 - psql and pgAdmin sessions, backup and dump scripts, monitoring, CI jobs, other applications sharing the server. Re-enabling it restores access, but that setting also restarts the server.

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=verbose as an environment variable of the application process (-DAZURE_LOG_LEVEL=verbose works too).

  • logging.level.com.azure: DEBUG in config/application.yml.

  • irt.logging.stdout-filter-level: DEBUG in config/application.yml, only needed to read the output on the console - logs/app.log already 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

PSQLException: Unable to load Authentication Plugin

The Azure jars are missing from the classpath. Confirm all eight are present and that loader.path points at the folder - step 6.

PostgreSQL driver 42.2 cannot be used with Microsoft Entra authentication, which requires 42.6 or later

The start-up guard found a driver too old to honour authenticationPluginClassName. Replace postgresql.jar, removing the old one - step 6.

FATAL: The access token has invalid format, or The server requested password-based authentication, but no password was provided, with no com.azure output at DEBUG

An old driver that the guard couldn’t read: it ignored authenticationPluginClassName, so the Azure library was never invoked. Check the driver manifest by hand - step 6.

FATAL: Service Principal oid mismatch for role[…​]

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 pgaadauth_list_principals(false) with az identity show --query principalId and link the role again.

FATAL: password authentication failed for user "…​"

The generic failure: Entra authentication not enabled, password authentication disabled while a password was sent, the role missing, or a role name case mismatch.

ManagedIdentityCredential authentication unavailable. Connection to IMDS endpoint cannot be established.

No managed identity on this host, or the metadata endpoint is unreachable - check that the identity is assigned and that no HTTP proxy intercepts 169.254.169.254.

ERROR: extension "pg_trgm" is not allow-listed for users, usually with HikariPool-1 - Connection …​ marked as broken because of SQLSTATE(0A000)

The extension isn’t in azure.extensions. Allow-list PG_TRGM even if the extension isn’t wanted - step 5.

ERROR: operator class "gin_trgm_ops" does not exist

pg_trgm is outside the application’s search_path. Set irt.db.append-pg-trgm-schema-to-search-path=true - step 5.

ERROR: No function matches the given name on a pgaadauth_* call

Not connected to the postgres database. Reconnect with dbname=postgres.

08P01: client sent partial pkt in startup phase

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.