Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b19ae99
Add an allowlist of server-mode password exec commands run without a …
dpage Oct 6, 2026
a5c3fea
Store the server-mode password exec command by name, converting exist…
dpage Oct 6, 2026
f151797
Resolve the server-mode password exec command per user, inheriting th…
dpage Oct 6, 2026
e82af2e
Accept and validate named password exec commands in the servers API.
dpage Oct 6, 2026
6f36b8a
Test the non-owner password exec API end to end and translate its err…
dpage Oct 6, 2026
6c8913d
Choose the server-mode password exec command from a list in the serve…
dpage Oct 6, 2026
5b2254d
Import and export server-mode password exec commands by name, and doc…
dpage Oct 6, 2026
25f5b99
Skip the legacy server-mode password exec conversion when pgAdmin run…
dpage Oct 6, 2026
0e741bd
Report server-mode password exec failures by command name without exp…
dpage Oct 6, 2026
53016b7
Run server-mode password exec commands with a reduced environment and…
dpage Oct 6, 2026
fabfcbc
Copy another user's shared server with no password exec command inste…
dpage Oct 6, 2026
93c3f33
Warn administrators which password exec environment variables are und…
dpage Oct 6, 2026
c1078af
Give each password exec instance its own lock so one slow command doe…
dpage Oct 6, 2026
2e68a61
Hide the password exec expiration in server mode when no commands are…
dpage Oct 6, 2026
6fbbf2d
Skip the shared server lookup in server mode when no password exec co…
dpage Oct 6, 2026
ebff031
Explain placeholder handling and inheritance after the server-mode pa…
dpage Oct 6, 2026
80cbfe0
Clean up the password exec test temporary directory and check convers…
dpage Oct 6, 2026
e8f9c0a
Log a malformed password exec passthrough setting, and keep SYSTEMROO…
dpage Oct 6, 2026
9c4e1eb
Run the server-mode password exec API test only in server-mode test r…
dpage Oct 6, 2026
c30cfd8
Postpone the server-mode password exec conversion until SERVER_PASSEX…
dpage Oct 12, 2026
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
9 changes: 9 additions & 0 deletions docs/en_US/import_export_servers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ imported server.

Password fields cannot be imported or exported.

In desktop mode, ``PasswordExecCommand`` is the shell command to run, as shown
in the example below. In server mode it must instead be the name of a command
defined in the ``SERVER_PASSEXEC_COMMANDS`` setting (see :ref:`config_py`), and
the same name is written when servers are exported. A server whose
``PasswordExecCommand`` is not the name of a configured command is still
imported, but without a password exec command, and a warning naming the server
is printed. ``PasswordExecExpiration`` is imported only together with a valid
command name.

The following example shows both a minimally defined and a fully defined server:

.. code-block:: python
Expand Down
61 changes: 58 additions & 3 deletions docs/en_US/server_dialog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,44 @@ Use the fields in the *Advanced* tab to configure a connection:
that will be used against the pg_database table to limit the databases that you see.
For example, you might enter: *live_db test_db* so that only live_db and test_db
are shown in the pgAdmin object explorer.
* Use the *Password exec command* field to specify a shell command to be executed
* In desktop mode, use the *Password exec command* field to specify a shell command to be executed
to retrieve a password to be used for SQL authentication. The ``stdout`` of the
command will be used as the SQL password. This may be useful when the password
should be generated as a transient authorization token instead of providing a
password when connecting in `PAM authentication <https://www.postgresql.org/docs/current/auth-pam.html>`_ scenarios.
You can pass server hostname, port and DB username to the password exec command as variable by providing placeholders
like ``%HOSTNAME%``, ``%PORT%`` and ``%USERNAME%`` which will be replace with the server connection information.
Example: ``/path/to/script --hostname %HOSTNAME% --port %PORT% --username %USERNAME%``
* In server mode, use the *Password exec command* drop-down list to select one
of the commands that the administrator has defined in the
``SERVER_PASSEXEC_COMMANDS`` setting (see :ref:`config_py`). Users cannot
enter their own command. The list offers *None* and each configured command
name, and is hidden if no commands have been configured. On a shared server,
users who do not own the server also see *Inherit from owner*, which is the
default for them and uses whatever command the owner has selected; selecting
*None* or a named command overrides it for that user only. If a selected
command is later removed from the configuration, the server is treated as
having no command and a warning is logged.

The command is run directly, without a shell, and does not receive the full
environment of the pgAdmin process. Only ``PATH``, ``HOME``, ``LANG``,
``LANGUAGE``, ``TZ``, ``TMPDIR``, ``TMP``, ``TEMP``, ``USER``, ``LOGNAME``
and any ``LC_*`` variable are passed through (and, on Windows,
``SYSTEMROOT``, ``SYSTEMDRIVE``, ``PATHEXT``, ``COMSPEC`` and ``WINDIR``),
together with any variables the administrator has named in the
``SERVER_PASSEXEC_ENV_PASSTHROUGH`` setting. The command also receives the
following variables describing the connection, which take precedence over
any variable of the same name:
``PGADMIN_PASSEXEC_HOST``, ``PGADMIN_PASSEXEC_PORT``,
``PGADMIN_PASSEXEC_USERNAME`` (the username that connection uses, so a
non-owner's own username on a shared server), ``PGADMIN_PASSEXEC_DATABASE``
(the maintenance database), ``PGADMIN_PASSEXEC_PGADMIN_USER`` (the pgAdmin
user making the connection) and ``PGADMIN_PASSEXEC_AUTH_SOURCE`` (how that
user authenticated to pgAdmin). The ``stdout`` of the command is used as the
SQL password.
* Use the *Password exec expiration* field to specify a maximum age, in seconds,
of the password generated with a *Password exec command*. If not specified,
of the password generated with a *Password exec command*. In server mode, this field is
enabled only when a named command is selected. If not specified,
the password will not expire until your pgAdmin session does.
Zero means the command will be executed for each new connection or reconnection that is made.
If the generated password is not valid indefinitely, set this value to slightly before it will expire.
Expand All @@ -234,7 +262,34 @@ Use the fields in the *Advanced* tab to configure a connection:
.. note:: The password file option is only supported when pgAdmin is using libpq
v10.0 or later to connect to the server.

.. note:: The Password exec option is only supported when pgAdmin is run in desktop mode.
.. note:: In server mode, the *Password exec command* setting is available
only when the administrator has defined at least one command in
``SERVER_PASSEXEC_COMMANDS``. Free-text commands are never run in server
mode, and when pgAdmin is upgraded, an existing free-text command is kept
only if it exactly matches a configured command (its arguments joined by
single spaces), in which case it is converted to that command's name; all
others are cleared and a warning is logged. This conversion cannot be
undone, so ``SERVER_PASSEXEC_COMMANDS`` must be configured before the
first start after upgrading. If it is still empty at that point, the
conversion is postponed: existing commands are left in the configuration
database unused, a warning giving the number of affected servers is
logged at each start, and they are converted or cleared on the first
start at which at least one command is configured. Placeholders such as
``%HOSTNAME%``, ``%PORT%`` and ``%USERNAME%`` are not substituted in
server mode, so a command containing them is converted only if the
configured arguments contain the same literal text. On a shared server, a
user who does not own the server and whose own command is cleared will
then inherit the owner's command, if the owner has one. The
``ENABLE_SERVER_PASS_EXEC_CMD`` setting is deprecated and ignored.

.. warning:: In server mode, only ``PGADMIN_PASSEXEC_PGADMIN_USER`` and
``PGADMIN_PASSEXEC_AUTH_SOURCE`` identify the pgAdmin user on whose behalf
the command is run. ``PGADMIN_PASSEXEC_HOST``, ``PGADMIN_PASSEXEC_PORT``,
``PGADMIN_PASSEXEC_USERNAME`` and ``PGADMIN_PASSEXEC_DATABASE`` are set by
whoever edits the server definition; on a shared server, a user who does
not own the server sets their own username. Commands should therefore
decide whether to issue a password based on the pgAdmin identity, and
treat the other four variables as untrusted input.

Click the *Post Connection SQL* tab to continue.

Expand Down
52 changes: 48 additions & 4 deletions web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,10 +1067,54 @@
SERVER_HEARTBEAT_TIMEOUT = 30 # In seconds

#############################################################################
# ENABLE_SERVER_PASS_EXEC_CMD is used to enable/disable Password exec command
# field in server properties. This is used to specify a shell command to be
# executed to retrieve a password to be used for server authentication.
# This setting is applicable only for server mode.
# SERVER_PASSEXEC_COMMANDS is an allowlist of named commands that may be used
# to retrieve a password for server authentication. Users pick a command by
# name in the server properties; they cannot supply their own. Each value is
# a list holding the executable followed by its arguments, for example:
#
# SERVER_PASSEXEC_COMMANDS = {
# 'vault': ['/usr/local/bin/get-pg-pass', '--ttl', '300']
# }
#
# Commands are run directly, without a shell, so no quoting or substitution
# is applied to the arguments. Connection details are passed in these
# environment variables: PGADMIN_PASSEXEC_HOST, PGADMIN_PASSEXEC_PORT,
# PGADMIN_PASSEXEC_USERNAME, PGADMIN_PASSEXEC_DATABASE,
# PGADMIN_PASSEXEC_PGADMIN_USER and PGADMIN_PASSEXEC_AUTH_SOURCE. Only the
# last two identify the pgAdmin user; the host, port, username and database
# are set by whoever edits the server, so commands must authorise on the
# pgAdmin identity and treat the other four as untrusted input.
# Names may not start with "__". An empty dictionary (the default) turns the
# feature off. This setting is applicable only for server mode.
#
# Upgrading from a release that allowed free-text commands in server mode:
# configure this BEFORE the first start after upgrading. On that start, an
# existing free-text command is converted to the name of the entry whose
# arguments, joined by single spaces, match it exactly, and every other
# command is cleared; the clearing cannot be undone. If the dictionary is
# empty at that start, the conversion is postponed: existing commands stay
# in the configuration database unused, a warning is logged at each start,
# and they are converted or cleared on the first start with at least one
# entry configured.
#############################################################################
SERVER_PASSEXEC_COMMANDS = {}

#############################################################################
# SERVER_PASSEXEC_ENV_PASSTHROUGH lists the names of additional environment
# variables to pass from pgAdmin's environment to the commands defined in
# SERVER_PASSEXEC_COMMANDS, for example ['VAULT_ADDR']. The commands do not
# receive pgAdmin's full environment; they get only PATH, HOME, LANG,
# LANGUAGE, TZ, TMPDIR, TMP, TEMP, USER, LOGNAME, any LC_* variable (and on
# Windows, SYSTEMROOT, SYSTEMDRIVE, PATHEXT, COMSPEC and WINDIR), the
# variables named here, and the PGADMIN_PASSEXEC_* variables, which always
# take precedence. Do not list variables that hold secrets the commands do
# not need. This setting is applicable only for server mode.
#############################################################################
SERVER_PASSEXEC_ENV_PASSTHROUGH = []

#############################################################################
# DEPRECATED and ignored: free-text password exec commands are never run in
# server mode. Use SERVER_PASSEXEC_COMMANDS instead.
#############################################################################
ENABLE_SERVER_PASS_EXEC_CMD = False

Expand Down
50 changes: 50 additions & 0 deletions web/migrations/versions/server_passexec_name_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""Add passexec_name to the server and sharedserver tables

In server mode the password exec command is chosen by name from the
administrator-defined SERVER_PASSEXEC_COMMANDS setting, rather than
being free text. This adds the column that holds the chosen name.
Existing passexec_cmd values are converted at application start up,
because that needs the configuration, which a migration cannot see.

Revision ID: server_passexec_name
Revises: normalize_locked_text_default
Create Date: 2026-10-06

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'server_passexec_name'
down_revision = 'normalize_locked_text_default'
branch_labels = None
depends_on = None


def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)

for table in ('server', 'sharedserver'):
if not inspector.has_table(table):
continue
existing_cols = {c['name'] for c in inspector.get_columns(table)}
if 'passexec_name' in existing_cols:
continue
with op.batch_alter_table(table) as batch_op:
batch_op.add_column(
sa.Column('passexec_name', sa.Text(), nullable=True))


def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
3 changes: 3 additions & 0 deletions web/pgadmin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,9 @@ def run_migration_for_others():
if not cli_mode:
delete_adhoc_servers()

from pgadmin.utils.passexec import server_passexec_startup
server_passexec_startup(app, cli_mode)

Mail(app)

# Don't bother paths when running in cli mode
Expand Down
4 changes: 3 additions & 1 deletion web/pgadmin/browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
VW_EDT_DEFAULT_PLACEHOLDER, NO_CACHE_CONTROL
from pgadmin.authenticate import AuthSourceManager
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.utils.passexec import get_server_passexec_commands

from pgadmin.user_login_check import pga_login_required

Expand Down Expand Up @@ -563,7 +564,8 @@ def utils():
shared_storage_list=shared_storage_list,
restricted_shared_storage_list=[] if current_user.has_role(
"Administrator") else restricted_shared_storage_list,
enable_server_passexec_cmd=config.ENABLE_SERVER_PASS_EXEC_CMD,
server_passexec_commands=sorted(get_server_passexec_commands())
if config.SERVER_MODE else [],
max_server_tags_allowed=config.MAX_SERVER_TAGS_ALLOWED,
llm_enabled=is_llm_enabled(),
), 200)
Expand Down
Loading
Loading