public inbox for [email protected]
help / color / mirror / Atom feedFrom: Greg Sabino Mullane <[email protected]>
To: Tom Lane <[email protected]>
Cc: Antonin Houska <[email protected]>
Cc: [email protected] <[email protected]>
Subject: Re: POC: Carefully exposing information without authentication
Date: Sat, 31 May 2025 00:48:13 -0400
Message-ID: <CAKAnmm+RQbLFv1F35ZTRZfMRigwN0LN0KKRocLLpwSshBTZxvg@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <CAKAnmm+T-CEDLmRezWfH-7ZEsFfD_kU2KY1TgB288K+wprB_4Q@mail.gmail.com>
<21076.1748617331@localhost>
<CAKAnmmJ77jeYZGXPBxb75U52ojNRUoKd6Za-T26xNPCouUeV8g@mail.gmail.com>
<[email protected]>
On Fri, May 30, 2025 at 9:34 PM Tom Lane <[email protected]> wrote:
> I think calling it in the postmaster is a nonstarter.
Thanks for the feedback. Please find attached version two, which moves the
code to the very start of BackendInitialize in
tcop/backend_startup.c. If we handle the request, we simply proc_exit and
avoid all the other backend startup stuff. So still a big win. I also made
a first rough pass at the documentation.
Cheers,
Greg
--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support
Attachments:
[application/octet-stream] 0002-Allow-specific-information-to-be-output-directly-by-Postgres.patch (11.7K, 3-0002-Allow-specific-information-to-be-output-directly-by-Postgres.patch)
download | inline diff:
From dab6ea7fd21aafbf5f87b163903e54f129f45e0b Mon Sep 17 00:00:00 2001
From: Greg Sabino Mullane <[email protected]>
Date: Sat, 31 May 2025 00:38:26 -0400
Subject: [PATCH] Allow specific information to be output directly by Postgres.
---
doc/src/sgml/config.sgml | 78 +++++++
src/backend/tcop/backend_startup.c | 191 ++++++++++++++++++
src/backend/utils/misc/guc_tables.c | 27 +++
src/backend/utils/misc/postgresql.conf.sample | 8 +
src/include/postmaster/postmaster.h | 4 +
5 files changed, 308 insertions(+)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f4a0191c55b..5774d78740f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1077,6 +1077,84 @@ include_dir 'conf.d'
</variablelist>
</sect2>
+ <sect2 id="runtime-config-expose-settings">
+ <title>Expose Settings</title>
+
+ <variablelist>
+
+ <varlistentry id="guc-expose-recovery" xreflabel="expose_recovery">
+ <term><varname>expose_recovery</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>expose_recovery</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables reporting if the server is in recovery mode without requiring
+ an authenticated login. Clients can send the string <literal>GET /replica</literal>
+ and will receive a 1 or 0. This is equivalent to logging in and running
+ <literal>SELECT pg_is_in_recovery()</literal>. A client can also send the
+ string <literal>HEAD /replica</literal> which will solely return an HTTP literal:
+ <literal>200</literal> if the server is in recovery, <literal>503</literal> if not.
+ (This allows a drop-in replacement to the same Patroni functionality)
+ Finally, a client can issue <literal>GET /info</literal> and receive the string
+ <literal>RECOVERY: </literal> followed by a 1 or 0.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-expose-sysid" xreflabel="expose_sysid">
+ <term><varname>expose_sysid</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>expose_sysid</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables reporting the system identifier of the cluster without requiring
+ an authenticated login. Clients can send the string <literal>GET /sysid</literal>
+ and will receive the numeric system identifier. This is a unique number generated
+ by each cluster when initdb is run.
+ </para>
+ <para>
+ A client can issue <literal>GET /info</literal> and receive the string
+ <literal>SYSID: </literal> followed by the numeric system identifier.
+ </para>
+ <para>
+ This feature is useful for determining if the server is the same server as previously
+ encountered. Note than primary and replica servers will share the same system
+ identifier.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-expose-version" xreflabel="expose_version">
+ <term><varname>expose_version</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>expose_version</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables reporting the numeric version of the Postgres cluster without requiring
+ an authenticated login. Clients can send the string <literal>GET /version</literal>
+ and will receive an integer representing the version.
+ </para>
+ <para>
+ A client can issue <literal>GET /info</literal> and receive the string
+ <literal>VERSION: </literal> followed by the numeric version.
+ </para>
+ <para>
+ This is particularly useful for non-Postgres systems (esp. security scanners) that
+ need a way to easily determine the version of Postgres in use without requiring
+ a Postgres client - or without needing any knowledge of the Postgres protocol at all.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </sect2>
+
<sect2 id="runtime-config-connection-authentication">
<title>Authentication</title>
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index a7d1fec981f..ce8a5087ec6 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -46,6 +46,10 @@
bool Trace_connection_negotiation = false;
uint32 log_connections = 0;
char *log_connections_string = NULL;
+bool expose_recovery = false;
+bool expose_sysid = false;
+bool expose_version = false;
+
/* Other globals */
@@ -65,6 +69,7 @@ static void SendNegotiateProtocolVersion(List *unrecognized_protocol_options);
static void process_startup_packet_die(SIGNAL_ARGS);
static void StartupPacketTimeoutHandler(void);
static bool validate_log_connections_options(List *elemlist, uint32 *flags);
+static bool ExposeInformation(pgsocket fd);
/*
* Entry point for a new backend process.
@@ -148,6 +153,15 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
StringInfoData ps_data;
MemoryContext oldcontext;
+ /*
+ * Possibly scan for a simple GET / HEAD request. If this is detected and
+ * handled, we are done and can immediately exit
+ */
+ if ((expose_recovery || expose_sysid || expose_version)
+ && ExposeInformation(client_sock->sock))
+ proc_exit(0);
+ /* Should we do exit(0) here, despite the warnings in ipc.c? */
+
/* Tell fd.c about the long-lived FD associated with the client_sock */
ReserveExternalFD();
@@ -1113,3 +1127,180 @@ assign_log_connections(const char *newval, void *extra)
{
log_connections = *((int *) extra);
}
+
+
+static
+bool
+ExposeInformation(pgsocket fd)
+{
+
+/*
+ * ExposeInformation
+ *
+ *
+ * Handle early socket probe before full backend startup.
+ * Responds to small set of predefined endpoints (e.g. GET /info)
+ *
+ * Requires at least one "expose_" GUC to be true.
+ *
+ * Returns true if endpoint is recognized.
+ */
+
+#define EXPOSE_MIN_QUERY 9 /* Shortest possible line: "Get /info" */
+#define EXPOSE_MAX_QUERY 16 /* Longest possible GET line */
+
+/* What information is being returned */
+ typedef enum
+ {
+ EXPOSE_NOTHING,
+ EXPOSE_HEAD_REPLICA,
+ EXPOSE_GET_ALL,
+ EXPOSE_GET_REPLICA,
+ EXPOSE_GET_SYSID,
+ EXPOSE_GET_VERSION,
+ } ReturnType;
+
+ typedef struct
+ {
+ const char *endpoint;
+ const bool *require;
+ ReturnType type;
+ } endpoint_action;
+
+ static endpoint_action endpoint_actions[] =
+ {
+ {
+ "HEAD /replica", &expose_recovery, EXPOSE_HEAD_REPLICA
+ },
+ {
+ "GET /replica", &expose_recovery, EXPOSE_GET_REPLICA
+ },
+ {
+ "GET /sysid", &expose_sysid, EXPOSE_GET_SYSID
+ },
+ {
+ "GET /version", &expose_version, EXPOSE_GET_VERSION
+ },
+ {
+ "GET /info", NULL, EXPOSE_GET_ALL
+ }
+ };
+
+ ssize_t n;
+ char buf[EXPOSE_MAX_QUERY + 1];
+ int type;
+
+ Assert(expose_recovery || expose_sysid || expose_version);
+
+ do
+ {
+ n = recv(fd, buf, EXPOSE_MAX_QUERY, MSG_PEEK);
+ } while (n < 0 && errno == EINTR);
+
+ /*
+ * Leave as soon as possible if not chance we are interested. We also
+ * simply return false for n == -1
+ */
+ if (n < EXPOSE_MIN_QUERY)
+ return false;
+
+ buf[n] = '\0';
+
+ type = EXPOSE_NOTHING;
+ for (int i = 0; i < lengthof(endpoint_actions); i++)
+ {
+ if (
+ strncmp(buf, endpoint_actions[i].endpoint, strlen(endpoint_actions[i].endpoint)) == 0
+ &&
+ (endpoint_actions[i].require == NULL
+ ||
+ *(endpoint_actions[i].require)
+ ))
+ {
+ type = endpoint_actions[i].type;
+ break;
+ }
+ }
+
+ if (type == EXPOSE_NOTHING)
+ return false;
+
+ {
+ static const char http_version[] = "HTTP/1.1";
+ static const char http_type[] = "Content-Type: text/plain";
+ static const char *http_conn = "Connection: close";
+ static const char http_len[] = "Content-Length";
+
+ StringInfoData msg;
+
+ if (type == EXPOSE_HEAD_REPLICA)
+ {
+ /*
+ * Caller only cares about the HTTP response code, so no content
+ * needed
+ */
+
+ initStringInfoExt(&msg, 64);
+
+ appendStringInfo(&msg,
+ "%s %s\r\n"
+ "%s\r\n"
+ "%s\r\n\r\n",
+ http_version,
+ (RecoveryInProgress() ? "200 OK" : "503 Service Unavailable"),
+ http_type,
+ http_conn
+ );
+ }
+ else
+ {
+ StringInfoData content;
+
+ initStringInfoExt(&content, 64);
+
+ if (expose_recovery && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_REPLICA))
+ appendStringInfo(&content, "%s%d\r\n",
+ type == EXPOSE_GET_ALL ? "RECOVERY: " : "",
+ RecoveryInProgress() ? 1 : 0);
+ if (expose_sysid && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_SYSID))
+ appendStringInfo(&content, "%s%lu\r\n",
+ type == EXPOSE_GET_ALL ? "SYSID: " : "",
+ GetSystemIdentifier());
+ if (expose_version && (type == EXPOSE_GET_ALL || type == EXPOSE_GET_VERSION))
+ appendStringInfo(&content, "%s%d\r\n",
+ type == EXPOSE_GET_ALL ? "VERSION: " : "",
+ PG_VERSION_NUM);
+
+ initStringInfoExt(&msg, 256);
+
+ appendStringInfo(&msg,
+ "%s 200 OK\r\n"
+ "%s\r\n"
+ "%s: %d\r\n"
+ "%s\r\n\r\n"
+ "%s",
+ http_version,
+ http_type,
+ http_len, content.len,
+ http_conn,
+ content.data
+ );
+
+ pfree(content.data);
+ }
+
+ do
+ {
+ n = send(fd, msg.data, msg.len, 0);
+ } while (n < 0 && errno == EINTR);
+
+ pfree(msg.data);
+
+ if (n < 0)
+ elog(DEBUG1, "could not send to client: %m");
+
+ return true;
+
+ }
+
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2f8cbd86759..38bea719011 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1701,6 +1701,33 @@ struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"expose_recovery", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+ gettext_noop("Exposes if the server is in recovery without a login."),
+ NULL
+ },
+ &expose_recovery,
+ false,
+ NULL, NULL, NULL
+ },
+ {
+ {"expose_sysid", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+ gettext_noop("Exposes the system identifier without a login."),
+ NULL
+ },
+ &expose_sysid,
+ false,
+ NULL, NULL, NULL
+ },
+ {
+ {"expose_version", PGC_SIGHUP, CLIENT_CONN_STATEMENT,
+ gettext_noop("Exposes the version without a login."),
+ NULL
+ },
+ &expose_version,
+ false,
+ NULL, NULL, NULL
+ },
{
{"array_nulls", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS,
gettext_noop("Enables input of NULL elements in arrays."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 87ce76b18f4..a16d3726824 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -91,6 +91,14 @@
# disconnection while running queries;
# 0 for never
+
+# - Expose information -
+
+#expose_recovery = off
+#expose_sysid = off
+#expose_version = off
+
+
# - Authentication -
#authentication_timeout = 1min # 1s-600s
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 92497cd6a0f..15662ce0059 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -70,6 +70,10 @@ extern PGDLLIMPORT bool restart_after_crash;
extern PGDLLIMPORT bool remove_temp_files_after_crash;
extern PGDLLIMPORT bool send_abort_for_crash;
extern PGDLLIMPORT bool send_abort_for_kill;
+extern PGDLLIMPORT bool expose_recovery;
+extern PGDLLIMPORT bool expose_sysid;
+extern PGDLLIMPORT bool expose_version;
+
#ifdef WIN32
extern PGDLLIMPORT HANDLE PostmasterHandle;
--
2.30.2
view thread (8+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected]
Subject: Re: POC: Carefully exposing information without authentication
In-Reply-To: <CAKAnmm+RQbLFv1F35ZTRZfMRigwN0LN0KKRocLLpwSshBTZxvg@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox