-
Notifications
You must be signed in to change notification settings - Fork 820
feat: add AWS IAM Database Authentication support for RDS/Aurora PosgreSQL #9579
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: For boto3 1.42.x, the botocore dependency is pinned to the same minor line:
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 Suggested change-botocore>=1.31.0
+botocore==1.42.*🤖 Prompt for AI Agents |
||
| certifi==2026.1.4 | ||
| cryptography==46.0.* | ||
| Flask-Babel==4.0.* | ||
|
|
||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
|
@@ -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, | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate required IAM fields when If ✅ 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 |
||
| connection_params=connection_params, | ||
| prepare_threshold=data.get('prepare_threshold', None), | ||
| tags=data.get('tags', None), | ||
|
|
@@ -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) | ||
| ) | ||
|
|
||
|
|
@@ -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: | ||
|
|
@@ -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: | ||
|
|
@@ -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: | ||
|
|
@@ -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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate AWS Region when IAM auth is enabled. 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 |
||
| },{ | ||
| 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' | ||
|
|
@@ -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'), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents