Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions docs/en_US/aws_iam_authentication.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
.. _aws_iam_authentication:

*******************************************
`AWS IAM Database Authentication`:index:
*******************************************

**Prerequisite:** AWS account with RDS/Aurora PostgreSQL and IAM configuration

Reference: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html

pgAdmin supports AWS IAM Database Authentication for connecting to Amazon RDS
and Aurora PostgreSQL databases. This feature allows you to authenticate using
AWS IAM credentials instead of a database password, providing enhanced security
through temporary authentication tokens.

How It Works
============

When IAM authentication is enabled:

1. pgAdmin generates a temporary authentication token using your AWS credentials
2. The token is valid for 15 minutes and is automatically refreshed as needed
3. SSL/TLS is automatically enforced (required by AWS for IAM authentication)
4. No database password needs to be stored or transmitted

Prerequisites
=============

Before using IAM authentication with pgAdmin, ensure the following:

AWS RDS/Aurora Configuration
----------------------------

* IAM Database Authentication must be enabled on your RDS instance or Aurora cluster
* A database user must be created and granted the ``rds_iam`` role:

.. code-block:: sql

CREATE USER your_username WITH LOGIN;
GRANT rds_iam TO your_username;

* An IAM policy must allow the ``rds-db:connect`` action for the database user

.. code-block:: json

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "rds-db:connect",
"Resource": "arn:aws:rds-db:region:account-id:dbuser:resource-id/db-user-name"
}
]
}

Local AWS Configuration
-----------------------

* AWS credentials must be configured on the machine running pgAdmin
* Credentials can be provided via:

* AWS credentials file (``~/.aws/credentials``)
* Environment variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``)
* IAM role (when running on EC2/ECS/Lambda)

* The ``botocore`` Python package must be installed (included with pgAdmin)

Configuring IAM Authentication in pgAdmin
=========================================

To connect to a PostgreSQL server using IAM authentication:

1. Open the *Server* dialog (right-click on *Servers* and select *Register* > *Server*)
2. In the *Connection* tab:

* Set *Host name/address* to your RDS/Aurora endpoint
* Set *Port* to the database port (default: 5432)
* Set *Maintenance database* to the database name
* Set *Username* to the IAM-enabled database user
* Enable *AWS IAM authentication*
* Leave the *Password* field empty

3. Configure the AWS-specific fields:

* *AWS Profile*: (Optional) The AWS profile name from your credentials file.
Leave empty to use the default profile or environment credentials.
* *AWS Region*: The AWS region where your RDS/Aurora instance is located
(e.g., ``us-east-1``, ``eu-west-1``).

4. Click *Save* to store the server configuration

Connection Behavior
===================

When connecting with IAM authentication:

* pgAdmin automatically generates a fresh IAM authentication token
* If the connection fails due to an expired token, pgAdmin will automatically
retry with a new token
* SSL mode is automatically set to ``require`` if not explicitly configured
* You will not be prompted for a password

Troubleshooting
===============

**Connection fails with "PAM authentication failed"**

* Verify the database user has the ``rds_iam`` role granted
* Ensure the IAM policy allows the ``rds-db:connect`` action
* Check that the AWS region is correctly configured

**Token generation fails**

* Verify AWS credentials are properly configured
* Check that the AWS profile name (if specified) exists in your credentials file
* Ensure the machine has network access to AWS STS endpoints

**SSL connection required**

* IAM authentication requires SSL. pgAdmin automatically enables SSL mode,
but ensure your RDS instance has SSL enabled and the client can establish
secure connections.

Limitations
===========

* IAM authentication tokens expire after 15 minutes. pgAdmin handles token
refresh automatically, but very long-running idle connections may need
to reconnect.
* This feature requires the ``botocore`` Python package.
* IAM authentication is only supported for Amazon RDS and Aurora PostgreSQL,
not for self-hosted PostgreSQL servers.

1 change: 1 addition & 0 deletions docs/en_US/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Mode is pre-configured for security.
restore_locked_user
ldap
kerberos
aws_iam_authentication
oauth2
webserver

Expand Down
7 changes: 7 additions & 0 deletions docs/en_US/server_dialog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ Use the fields in the *Connection* tab to configure a connection:
authenticating with the server.
* When *Kerberos authentication?* is set to *True*, pgAdmin will try to connect
the PostgreSQL server using Kerberos authentication.
* When *AWS IAM authentication?* is set to *True*, pgAdmin will use AWS IAM
Database Authentication to connect to Amazon RDS or Aurora PostgreSQL.
For more information, see :ref:`AWS IAM Database Authentication <aws_iam_authentication>`.

* *AWS Profile*: (Optional) The AWS profile name from your credentials file.
* *AWS Region*: The AWS region where your RDS/Aurora instance is located.

Comment on lines +73 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the AWS Role ARN field to match the UI.
The Connection tab now has an optional AWS Role ARN field, but it isn’t documented here. Please add a bullet so users know how/when to use it.

Doc snippet
   * *AWS Profile*: (Optional) The AWS profile name from your credentials file.
   * *AWS Region*: The AWS region where your RDS/Aurora instance is located.
+  * *AWS Role ARN*: (Optional) IAM role ARN for cross-account or assumed-role access.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* When *AWS IAM authentication?* is set to *True*, pgAdmin will use AWS IAM
Database Authentication to connect to Amazon RDS or Aurora PostgreSQL.
For more information, see :ref:`AWS IAM Database Authentication <aws_iam_authentication>`.
* *AWS Profile*: (Optional) The AWS profile name from your credentials file.
* *AWS Region*: The AWS region where your RDS/Aurora instance is located.
* When *AWS IAM authentication?* is set to *True*, pgAdmin will use AWS IAM
Database Authentication to connect to Amazon RDS or Aurora PostgreSQL.
For more information, see :ref:`AWS IAM Database Authentication <aws_iam_authentication>`.
* *AWS Profile*: (Optional) The AWS profile name from your credentials file.
* *AWS Region*: The AWS region where your RDS/Aurora instance is located.
* *AWS Role ARN*: (Optional) IAM role ARN for cross-account or assumed-role access.
🤖 Prompt for AI Agents
In `@docs/en_US/server_dialog.rst` around lines 73 - 79, Add a new bullet
describing the optional "AWS Role ARN" field in the same list as "AWS Profile"
and "AWS Region": explain that "AWS Role ARN" is the IAM role Amazon Resource
Name to assume when using AWS IAM Database Authentication, when to provide it
(e.g., to assume a role different from the profile’s default), and mention it is
optional and only used when "AWS IAM authentication?" is True; edit the
paragraph around the Connection tab bullets to include this new bullet so it
matches the UI.

* Use the *Password* field to provide a password that will be supplied when
authenticating with the server.
* Check the box next to *Save password?* to instruct pgAdmin to save the
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ azure-mgmt-resource==24.0.0
azure-mgmt-subscription==3.1.1
bcrypt==5.0.*
boto3==1.42.*
botocore>=1.31.0
Comment on lines 21 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

boto3 1.42.x botocore version requirement dependency

💡 Result:

For boto3 1.42.x, the botocore dependency is pinned to the same minor line:

  • botocore >= 1.42.* and < 1.43.0 (upper-bound prevents pulling the next minor series). (secure.software)

Example (from 1.42.3 metadata as reported by a dependency scanner):

Citations:


Tighten botocore constraint to match boto3's internal requirement.

boto3 1.42.x internally requires botocore >= 1.42.0, < 1.43.0. The current botocore>=1.31.0 is unnecessarily loose and could allow incompatible versions in environments where package resolvers don't strictly enforce transitive constraints. Align it with boto3's minor series:

Suggested change
-botocore>=1.31.0
+botocore==1.42.*
🤖 Prompt for AI Agents
In `@requirements.txt` around lines 21 - 22, Update the botocore version
constraint in requirements.txt to match boto3 1.42.x's internal requirement so
transitive resolution cannot pick an incompatible botocore; replace the loose
"botocore>=1.31.0" entry with a tight constraint "botocore>=1.42.0,<1.43.0" to
align with the installed boto3==1.42.* dependency.

certifi==2026.1.4
cryptography==46.0.*
Flask-Babel==4.0.*
Expand Down
41 changes: 41 additions & 0 deletions web/migrations/versions/7ce2161fb957_add_iam_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""Add AWS IAM authentication support

Added use_iam_auth, aws_profile, aws_region, and aws_role_arn columns
to server configuration for AWS RDS/Aurora IAM authentication.

Revision ID: 7ce2161fb957
Revises: 018e16dad6aa
Create Date: 2026-01-30 11:00:00.000000

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = '7ce2161fb957'
down_revision = '018e16dad6aa'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('server', sa.Column('use_iam_auth', sa.Boolean(),
server_default='0', nullable=False))
op.add_column('server', sa.Column('aws_profile', sa.String(length=64)))
op.add_column('server', sa.Column('aws_region', sa.String(length=32)))
op.add_column('server', sa.Column('aws_role_arn', sa.String(length=256)))
# ### end Alembic commands ###


def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
30 changes: 24 additions & 6 deletions web/pgadmin/browser/server_groups/servers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,10 @@ def update(self, gid, sid):
'shared': 'shared',
'shared_username': 'shared_username',
'kerberos_conn': 'kerberos_conn',
'use_iam_auth': 'use_iam_auth',
'aws_profile': 'aws_profile',
'aws_region': 'aws_region',
'aws_role_arn': 'aws_role_arn',
'connection_params': 'connection_params',
'prepare_threshold': 'prepare_threshold',
'tags': 'tags',
Expand Down Expand Up @@ -1094,6 +1098,10 @@ def properties(self, gid, sid):
'tunnel_authentication': tunnel_authentication,
'tunnel_keep_alive': tunnel_keep_alive,
'kerberos_conn': bool(server.kerberos_conn),
'use_iam_auth': bool(server.use_iam_auth) if hasattr(server, 'use_iam_auth') else False,
'aws_profile': server.aws_profile if hasattr(server, 'aws_profile') and server.aws_profile else None,
'aws_region': server.aws_region if hasattr(server, 'aws_region') and server.aws_region else None,
'aws_role_arn': server.aws_role_arn if hasattr(server, 'aws_role_arn') and server.aws_role_arn else None,
'gss_authenticated': manager.gss_authenticated,
'gss_encrypted': manager.gss_encrypted,
'cloud_status': server.cloud_status,
Expand Down Expand Up @@ -1225,6 +1233,10 @@ def create(self, gid):
passexec_cmd=data.get('passexec_cmd', None),
passexec_expiration=data.get('passexec_expiration', None),
kerberos_conn=1 if data.get('kerberos_conn', False) else 0,
use_iam_auth=1 if data.get('use_iam_auth', False) else 0,
aws_profile=data.get('aws_profile', None),
aws_region=data.get('aws_region', None),
aws_role_arn=data.get('aws_role_arn', None),
Comment on lines +1236 to +1239
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate required IAM fields when use_iam_auth is enabled.

If aws_region (and potentially host/port when using service files) is missing, token generation fails later with a generic error. A small server‑side check gives clearer feedback.

✅ Add minimal validation
         for arg in required_args:
             if arg not in data:
                 return make_json_response(
                     status=410,
                     success=0,
                     errormsg=gettext(
                         "Could not find the required parameter ({})."
                     ).format(arg)
                 )
+
+        if data.get('use_iam_auth') and not data.get('aws_region'):
+            return make_json_response(
+                status=400,
+                success=0,
+                errormsg=gettext("AWS region is required when IAM auth is enabled.")
+            )
🤖 Prompt for AI Agents
In `@web/pgadmin/browser/server_groups/servers/__init__.py` around lines 1236 -
1239, When use_iam_auth is set (use_iam_auth key truthy) validate required
IAM-related fields before constructing the server entry: ensure aws_region is
provided and non-empty and, if your flow expects a service file or uses
host/port for token generation, ensure host and port are present as well; raise
a clear error (or return a validation response) referencing use_iam_auth so
callers see "aws_region (and host/port when using service files) required when
use_iam_auth=true". Update the place building the server from data (where
use_iam_auth, aws_profile, aws_region, aws_role_arn are read) to perform these
checks and return a user-friendly validation message rather than allowing token
generation to fail later.

connection_params=connection_params,
prepare_threshold=data.get('prepare_threshold', None),
tags=data.get('tags', None),
Expand Down Expand Up @@ -1455,7 +1467,7 @@ def connect(self, gid, sid, is_qt=False, server=None):
establish the connection OR just connect the server and do not
store the password.
"""
current_app.logger.info(
current_app.logger.error(
'Connection Request for server#{0}'.format(sid)
)

Expand Down Expand Up @@ -1543,9 +1555,11 @@ def connect(self, gid, sid, is_qt=False, server=None):
except Exception as e:
current_app.logger.exception(e)
return internal_server_error(errormsg=str(e))
# Skip password prompt for Kerberos and IAM authentication
use_iam_auth = getattr(server, 'use_iam_auth', False)
if 'password' not in data and (server.kerberos_conn is False or
server.kerberos_conn is None):

server.kerberos_conn is None) and \
not use_iam_auth:
passfile_param = None
if hasattr(server, 'connection_params') and \
'passfile' in server.connection_params:
Expand Down Expand Up @@ -1595,8 +1609,10 @@ def connect(self, gid, sid, is_qt=False, server=None):
server_types=ServerType.types()
)
except Exception as e:
# Don't prompt for password on IAM auth failures - just show error
should_prompt_password = not server.save_password and not use_iam_auth
return self.get_response_for_password(
server, 401, not server.save_password, prompt_tunnel_password,
server, 401, should_prompt_password, prompt_tunnel_password,
getattr(e, 'message', str(e)))

if not status:
Expand All @@ -1607,8 +1623,10 @@ def connect(self, gid, sid, is_qt=False, server=None):
if errmsg.find('Ticket expired') != -1:
return internal_server_error(errmsg)

# Don't prompt for password on IAM auth failures - just show error
should_prompt_password = not server.save_password and not use_iam_auth
return self.get_response_for_password(
server, 401, not server.save_password,
server, 401, should_prompt_password,
prompt_tunnel_password, errmsg)
else:
if save_password and config.ALLOW_SAVE_PASSWORD:
Expand Down Expand Up @@ -1652,7 +1670,7 @@ def connect(self, gid, sid, is_qt=False, server=None):

return internal_server_error(errormsg=e.message)

current_app.logger.info('Connection Established for server: \
current_app.logger.error('Connection Established for server: \
%s - %s' % (server.id, server.name))
# Update the recovery and wal pause option for the server
# if connected successfully
Expand Down
36 changes: 31 additions & 5 deletions web/pgadmin/browser/server_groups/servers/static/js/server.ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,37 @@ export default class ServerSchema extends BaseUISchema {
},{
id: 'gss_encrypted', label: gettext('GSS encrypted?'), type: 'switch',
group: gettext('Connection'), mode: ['properties'], visible: obj.isConnected,
},{
id: 'use_iam_auth', label: gettext('AWS IAM authentication?'), type: 'switch',
group: gettext('Connection'), mode: ['create', 'edit'],
disabled: obj.isShared,
helpMessage: gettext('Use AWS IAM authentication tokens for RDS/Aurora PostgreSQL databases')
},{
id: 'aws_profile', label: gettext('AWS Profile'), type: 'text',
group: gettext('Connection'), mode: ['create', 'edit'],
deps: ['use_iam_auth'],
disabled: function(state) { return !state.use_iam_auth; },
readonly: obj.isConnected,
helpMessage: gettext('AWS profile name for credentials (leave empty for default)')
},{
id: 'aws_region', label: gettext('AWS Region'), type: 'text',
group: gettext('Connection'), mode: ['create', 'edit'],
deps: ['use_iam_auth'],
disabled: function(state) { return !state.use_iam_auth; },
readonly: obj.isConnected,
helpMessage: gettext('AWS region where the database is located (e.g., us-east-1)')
},{
id: 'aws_role_arn', label: gettext('AWS Role ARN (Optional)'), type: 'text',
group: gettext('Connection'), mode: ['create', 'edit'],
deps: ['use_iam_auth'],
disabled: function(state) { return !state.use_iam_auth; },
readonly: obj.isConnected,
helpMessage: gettext('IAM role ARN for cross-account or assumed role access')
Comment on lines +385 to +409
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate AWS Region when IAM auth is enabled.
Users can currently save a server with IAM enabled but no region, which will likely cause token generation failures. Consider validating aws_region when use_iam_auth is true (or explicitly marking it optional if the backend derives it).

Possible validation addition (in validate())
+    if (state.use_iam_auth) {
+      if (isEmptyString(state.aws_region)) {
+        errmsg = gettext('AWS Region must be specified when IAM authentication is enabled.');
+        setError('aws_region', errmsg);
+        return true;
+      } else {
+        setError('aws_region', null);
+      }
+    }
🤖 Prompt for AI Agents
In `@web/pgadmin/browser/server_groups/servers/static/js/server.ui.js` around
lines 385 - 409, Add a validation rule in the validate() function to require
aws_region when use_iam_auth is true: when state.use_iam_auth is truthy, ensure
state.aws_region is non-empty and push a validation error for the field id
'aws_region' (or return validation failure) so the form cannot be saved without
an AWS region; reference the existing form field ids 'use_iam_auth' and
'aws_region' and respect obj.isConnected/read-only handling already present.

},{
id: 'password', label: gettext('Password'), type: 'password',
group: gettext('Connection'),
mode: ['create', 'edit'],
deps: ['kerberos_conn', 'save_password'],
deps: ['kerberos_conn', 'use_iam_auth', 'save_password'],
controlProps: {
maxLength: null,
autoComplete: 'new-password'
Expand All @@ -395,17 +421,17 @@ export default class ServerSchema extends BaseUISchema {
return false;
return state.connected || !state.save_password;
},
disabled: function(state) {return state.kerberos_conn;},
helpMessage: gettext('In edit mode the password field is enabled only if Save Password is set to true.')
disabled: function(state) {return state.kerberos_conn || state.use_iam_auth;},
helpMessage: gettext('In edit mode the password field is enabled only if Save Password is set to true. Password is not required for Kerberos or IAM authentication.')
},{
id: 'save_password', label: gettext('Save password?'),
type: 'switch', group: gettext('Connection'), mode: ['create', 'edit'],
deps: ['kerberos_conn'],
deps: ['kerberos_conn', 'use_iam_auth'],
readonly: function(state) {
return state.connected;
},
disabled: function(state) {
return !current_user.allow_save_password || state.kerberos_conn;
return !current_user.allow_save_password || state.kerberos_conn || state.use_iam_auth;
},
},{
id: 'role', label: gettext('Role'), type: 'text', group: gettext('Connection'),
Expand Down
7 changes: 6 additions & 1 deletion web/pgadmin/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
#
##########################################################################

SCHEMA_VERSION = 49
SCHEMA_VERSION = 50

##########################################################################
#
Expand Down Expand Up @@ -256,6 +256,11 @@ class Server(db.Model):
shared = db.Column(db.Boolean(), nullable=False)
shared_username = db.Column(db.String(64), nullable=True)
kerberos_conn = db.Column(db.Boolean(), nullable=False, default=0)
# AWS IAM Authentication
use_iam_auth = db.Column(db.Boolean(), nullable=False, default=False)
aws_profile = db.Column(db.String(64), nullable=True)
aws_region = db.Column(db.String(32), nullable=True)
aws_role_arn = db.Column(db.String(256), nullable=True)
cloud_status = db.Column(db.Integer(), nullable=False, default=0)
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
prepare_threshold = db.Column(db.Integer(), nullable=True)
Expand Down
Loading