From 5db9a6bb286db74f85c6869d04d9db12f9836cce Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Wed, 17 Jun 2026 12:23:50 +0000 Subject: [PATCH] Add pg_hosts_file_rules() to inspect the hosts configuration file --- doc/src/sgml/system-views.sgml | 148 +++++++++++++++++++++++++ src/backend/catalog/system_views.sql | 5 + src/backend/libpq/be-secure-common.c | 14 ++- src/backend/utils/adt/hbafuncs.c | 158 +++++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 7 ++ src/include/libpq/hba.h | 1 + src/test/regress/expected/rules.out | 11 ++ src/test/ssl/t/004_sni.pl | 18 +++ 8 files changed, 359 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 2ebec6928d5..e71c2e5edb5 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -101,6 +101,11 @@ summary of client authentication configuration file contents + + pg_hosts_file_rules + summary of hosts configuration file contents + + pg_ident_file_mappings summary of client user name mapping configuration file contents @@ -1558,6 +1563,149 @@ AND c1.path[c2.level] = c2.path[c2.level]; + + <structname>pg_hosts_file_rules</structname> + + + pg_hosts_file_rules + + + + The view pg_hosts_file_rules provides a summary of + the contents of the hosts configuration file, which maps incoming TLS + server name indication (SNI) host names to SSL certificate configuration. + A row appears in this view for each entry in the file, with annotations + indicating whether the entry could be parsed successfully. + + + + This view can be helpful for checking whether planned changes in the + hosts configuration file will work, or for diagnosing a previous failure. + Note that this view reports on the current contents + of the file, not on what was last loaded by the server. + + + + By default, the pg_hosts_file_rules view can be + read only by superusers. + + + + <structname>pg_hosts_file_rules</structname> Columns + + + + + Column Type + + + Description + + + + + + + + rule_number int4 + + + Number of this rule, if valid, otherwise NULL + + + + + + file_name text + + + Name of the file containing this rule + + + + + + line_number int4 + + + Line number of this rule in file_name + + + + + + hostnames text[] + + + List of host name(s) to which this rule applies + + + + + + ssl_certificate text + + + Path to the SSL certificate file used for these host names + + + + + + ssl_key text + + + Path to the SSL private key file used for these host names + + + + + + ssl_ca text + + + Path to the SSL certificate authority file, or null if not specified + + + + + + passphrase_command text + + + Command used to obtain the private key passphrase, or null if not + specified + + + + + + passphrase_command_reload bool + + + Whether the passphrase command is run on configuration reload + + + + + + error text + + + If not null, an error message indicating why this + entry could not be processed + + + + +
+ + + Usually, a row reflecting an incorrect entry will have values for only + the line_number and error fields. + +
+ <structname>pg_ident_file_mappings</structname> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 8f129baec90..983a2537516 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -667,6 +667,11 @@ CREATE VIEW pg_hba_file_rules AS REVOKE ALL ON pg_hba_file_rules FROM PUBLIC; +CREATE VIEW pg_hosts_file_rules AS + SELECT * FROM pg_hosts_file_rules() AS A; + +REVOKE ALL ON pg_hosts_file_rules FROM PUBLIC; + CREATE VIEW pg_ident_file_mappings AS SELECT * FROM pg_ident_file_mappings() AS A; diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c index 6ec887b8a47..9de816c0d9e 100644 --- a/src/backend/libpq/be-secure-common.c +++ b/src/backend/libpq/be-secure-common.c @@ -29,8 +29,6 @@ #include "utils/builtins.h" #include "utils/guc.h" -static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel); - /* * Run ssl_passphrase_command * @@ -191,10 +189,11 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart) * the TLS backend. Validation of the parsed values is left for the TLS backend * to implement. */ -static HostsLine * +HostsLine * parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) { HostsLine *parsedline; + char **err_msg = &tok_line->err_msg; List *tokens; ListCell *field; AuthToken *token; @@ -217,6 +216,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) if ((tokens->length > 1) && (strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0)) { + *err_msg = "default and non-SNI entries cannot be mixed with other entries"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("default and non-SNI entries cannot be mixed with other entries"), @@ -232,6 +232,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) field = lnext(tok_line->fields, field); if (!field) { + *err_msg = "missing entry at end of line"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("missing entry at end of line"), @@ -242,6 +243,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); if (tokens->length > 1) { + *err_msg = "multiple values specified for SSL certificate"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("multiple values specified for SSL certificate"), @@ -256,6 +258,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) field = lnext(tok_line->fields, field); if (!field) { + *err_msg = "missing entry at end of line"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("missing entry at end of line"), @@ -266,6 +269,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); if (tokens->length > 1) { + *err_msg = "multiple values specified for SSL key"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("multiple values specified for SSL key"), @@ -283,6 +287,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); if (tokens->length > 1) { + *err_msg = "multiple values specified for SSL CA"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("multiple values specified for SSL CA"), @@ -300,6 +305,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); if (tokens->length > 1) { + *err_msg = "multiple values specified for SSL passphrase command"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("multiple values specified for SSL passphrase command"), @@ -328,6 +334,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) */ if (lnext(tok_line->fields, field)) { + *err_msg = "extra fields at end of line"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("extra fields at end of line"), @@ -338,6 +345,7 @@ parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload)) { + *err_msg = "incorrect syntax for boolean value SSL_passphrase_cmd_reload"; ereport(elevel, errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"), diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c index bd23eda3f79..b17d2b5bc12 100644 --- a/src/backend/utils/adt/hbafuncs.c +++ b/src/backend/utils/adt/hbafuncs.c @@ -30,6 +30,10 @@ static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, int rule_number, char *filename, int lineno, HbaLine *hba, const char *err_msg); static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc); +static void fill_hosts_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, + int rule_number, const char *filename, int lineno, + HostsLine *host, const char *err_msg); +static void fill_hosts_view(Tuplestorestate *tuple_store, TupleDesc tupdesc); static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, int map_number, char *filename, int lineno, IdentLine *ident, const char *err_msg); @@ -448,6 +452,160 @@ pg_hba_file_rules(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +/* Number of columns in pg_hosts_file_rules view */ +#define NUM_PG_HOSTS_FILE_RULES_ATTS 10 + +/* + * fill_hosts_line + * Build one row of pg_hosts_file_rules view, add it to tuplestore. + * + * rule_number: unique identifier among valid rules (ignored when err_msg set) + * filename, lineno: always valid + * host: parsed entry (NULL when err_msg is set) + * err_msg: error message (NULL if none) + */ +static void +fill_hosts_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, + int rule_number, const char *filename, int lineno, + HostsLine *host, const char *err_msg) +{ + Datum values[NUM_PG_HOSTS_FILE_RULES_ATTS]; + bool nulls[NUM_PG_HOSTS_FILE_RULES_ATTS]; + HeapTuple tuple; + int index; + + Assert(tupdesc->natts == NUM_PG_HOSTS_FILE_RULES_ATTS); + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + index = 0; + + /* rule_number, nothing on error */ + if (err_msg) + nulls[index++] = true; + else + values[index++] = Int32GetDatum(rule_number); + + /* file_name */ + values[index++] = CStringGetTextDatum(filename); + + /* line_number */ + values[index++] = Int32GetDatum(lineno); + + if (host != NULL) + { + /* hostnames text[] */ + if (host->hostnames) + values[index++] = PointerGetDatum(strlist_to_textarray(host->hostnames)); + else + nulls[index++] = true; + + /* ssl_certificate */ + if (host->ssl_cert) + values[index++] = CStringGetTextDatum(host->ssl_cert); + else + nulls[index++] = true; + + /* ssl_key */ + if (host->ssl_key) + values[index++] = CStringGetTextDatum(host->ssl_key); + else + nulls[index++] = true; + + /* ssl_ca */ + if (host->ssl_ca) + values[index++] = CStringGetTextDatum(host->ssl_ca); + else + nulls[index++] = true; + + /* passphrase_command */ + if (host->ssl_passphrase_cmd) + values[index++] = CStringGetTextDatum(host->ssl_passphrase_cmd); + else + nulls[index++] = true; + + /* passphrase_command_reload */ + values[index++] = BoolGetDatum(host->ssl_passphrase_reload); + } + else + { + /* no parse result: null the per-host columns (indexes 3..8) */ + memset(&nulls[3], true, (NUM_PG_HOSTS_FILE_RULES_ATTS - 4) * sizeof(bool)); + } + + /* error */ + if (err_msg) + values[NUM_PG_HOSTS_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg); + else + nulls[NUM_PG_HOSTS_FILE_RULES_ATTS - 1] = true; + + tuple = heap_form_tuple(tupdesc, values, nulls); + tuplestore_puttuple(tuple_store, tuple); +} + +/* + * fill_hosts_view + * Read the pg_hosts file and fill the tuplestore with view records. + */ +static void +fill_hosts_view(Tuplestorestate *tuple_store, TupleDesc tupdesc) +{ + FILE *file; + List *hosts_lines = NIL; + ListCell *line; + int rule_number = 0; + MemoryContext hostscxt; + MemoryContext oldcxt; + + file = open_auth_file(HostsFileName, DEBUG3, 0, NULL); + if (file == NULL) + return; + + tokenize_auth_file(HostsFileName, file, &hosts_lines, DEBUG3, 0); + + hostscxt = AllocSetContextCreate(CurrentMemoryContext, + "hosts parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hostscxt); + foreach(line, hosts_lines) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line); + HostsLine *hostsline = NULL; + + if (tok_line->err_msg == NULL) + hostsline = parse_hosts_line(tok_line, DEBUG3); + + if (tok_line->err_msg == NULL) + rule_number++; + + fill_hosts_line(tuple_store, tupdesc, rule_number, + tok_line->file_name, tok_line->line_num, hostsline, + tok_line->err_msg); + } + + free_auth_file(file, 0); + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(hostscxt); +} + +/* + * pg_hosts_file_rules + * + * SQL-accessible SRF returning all entries in the pg_hosts configuration file. + */ +Datum +pg_hosts_file_rules(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsi; + + InitMaterializedSRF(fcinfo, 0); + + rsi = (ReturnSetInfo *) fcinfo->resultinfo; + fill_hosts_view(rsi->setResult, rsi->setDesc); + + PG_RETURN_NULL(); +} + /* Number of columns in pg_ident_file_mappings view */ #define NUM_PG_IDENT_FILE_MAPPINGS_ATTS 7 diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index be157a5fbe9..37c9020e92d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6605,6 +6605,13 @@ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}', prosrc => 'pg_hba_file_rules', proacl => '{POSTGRES=X}' }, +{ oid => '8801', descr => 'show pg_hosts configuration file rules', + proname => 'pg_hosts_file_rules', prorows => '1000', proretset => 't', + provolatile => 'v', prorettype => 'record', proargtypes => '', + proallargtypes => '{int4,text,int4,_text,text,text,text,text,bool,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o,o}', + proargnames => '{rule_number,file_name,line_number,hostnames,ssl_certificate,ssl_key,ssl_ca,passphrase_command,passphrase_command_reload,error}', + prosrc => 'pg_hosts_file_rules', proacl => '{POSTGRES=X}' }, { oid => '6250', descr => 'show pg_ident.conf mappings', proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 4aa6258a345..c9fa8d8f1dc 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -194,6 +194,7 @@ extern int check_usermap(const char *usermap_name, const char *pg_user, const char *system_user, bool case_insensitive); extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel); +extern HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel); extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel); extern FILE *open_auth_file(const char *filename, int elevel, int depth, char **err_msg); diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index a65a5bf0c4f..0a392f8ec1f 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1373,6 +1373,17 @@ pg_hba_file_rules| SELECT rule_number, options, error FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error); +pg_hosts_file_rules| SELECT rule_number, + file_name, + line_number, + hostnames, + ssl_certificate, + ssl_key, + ssl_ca, + passphrase_command, + passphrase_command_reload, + error + FROM pg_hosts_file_rules() a(rule_number, file_name, line_number, hostnames, ssl_certificate, ssl_key, ssl_ca, passphrase_command, passphrase_command_reload, error); pg_ident_file_mappings| SELECT map_number, file_name, line_number, diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl index f1a135bb3c8..e39dd4951c6 100644 --- a/src/test/ssl/t/004_sni.pl +++ b/src/test/ssl/t/004_sni.pl @@ -454,4 +454,22 @@ $result = $node->restart(fail_ok => 1); is($result, 0, 'pg_hosts.conf: restart fails with non-boolean value in boolean field'); +# pg_hosts_file_rules() reflects the parsed conf file. Write a known-good +# configuration so the SRF reports at least one valid entry. +$node->append_conf('pg_hba.conf', "local all all trust"); +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + 'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt'); +$node->restart; +my $rules = $node->safe_psql('postgres', + "SELECT count(*) FROM pg_hosts_file_rules() WHERE error IS NULL"); +ok($rules >= 1, "pg_hosts_file_rules: valid conf entries are reported"); + +# A malformed entry shows up as an error row, not a thrown error. +$node->append_conf('pg_hosts.conf', "this line has too many fields a b c d e f"); +$node->reload; +my $errs = $node->safe_psql('postgres', + "SELECT count(*) FROM pg_hosts_file_rules() WHERE error IS NOT NULL"); +ok($errs >= 1, "pg_hosts_file_rules: malformed conf entry becomes an error row"); + done_testing(); -- 2.43.0