Skip to content

Commit 9ea8dd6

Browse files
committed
PG-1667 Validate vault key provider engine type
pg_tde supports only Key/Value version 2 engine type for Hashicorp Vault. Add validation for that by quering mountpoint metadata.
1 parent 09b2af7 commit 9ea8dd6

File tree

5 files changed

+273
-34
lines changed

5 files changed

+273
-34
lines changed

ci_scripts/setup-keyring-servers.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export VAULT_ROOT_TOKEN_FILE=$(mktemp)
2525
jq -r .root_token "$CLUSTER_INFO" > "$VAULT_ROOT_TOKEN_FILE"
2626
export VAULT_CACERT_FILE=$(jq -r .ca_cert_path "$CLUSTER_INFO")
2727
rm "$CLUSTER_INFO"
28+
29+
## We need to enable key/value version 1 engine for just for tests
30+
vault secrets enable -ca-cert="$VAULT_CACERT_FILE" -path=kv-v1 -version=1 kv
31+
2832
if [ -v GITHUB_ACTIONS ]; then
2933
echo "VAULT_ROOT_TOKEN_FILE=$VAULT_ROOT_TOKEN_FILE" >> $GITHUB_ENV
3034
echo "VAULT_CACERT_FILE=$VAULT_CACERT_FILE" >> $GITHUB_ENV

contrib/pg_tde/documentation/docs/global-key-provider-configuration/vault.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ For more information on related functions, see the link below:
4141

4242
[Percona pg_tde Function Reference](../functions.md){.md-button}
4343

44+
## Required permissions
45+
`pg_tde` requires given permissions on listed Vault's API endpoints
46+
* `sys/mounts/<mount>` - **read** permissions
47+
* `<mount>/data/*` - **create**, **read** permissions
48+
* `<mount>/metadata` - **list** permissions
49+
50+
!!! note
51+
For more information on Vault permissions, see the [following documentation](https://developer.hashicorp.com/vault/docs/concepts/policies).
52+
4453
## Next steps
4554

4655
[Global Principal Key Configuration :material-arrow-right:](set-principal-key.md){.md-button}

contrib/pg_tde/expected/vault_v2_test.out

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
CREATE EXTENSION pg_tde;
22
\getenv root_token_file VAULT_ROOT_TOKEN_FILE
33
\getenv cacert_file VAULT_CACERT_FILE
4-
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'DUMMY-TOKEN', :'root_token_file', :'cacert_file');
5-
pg_tde_add_database_key_provider_vault_v2
6-
-------------------------------------------
7-
8-
(1 row)
9-
10-
-- FAILS
11-
SELECT pg_tde_create_key_using_database_key_provider('vault-v2-key', 'vault-incorrect');
12-
ERROR: Invalid HTTP response from keyring provider "vault-incorrect": 404
13-
CREATE TABLE test_enc(
14-
id SERIAL,
15-
k INTEGER DEFAULT '0' NOT NULL,
16-
PRIMARY KEY (id)
17-
) USING tde_heap;
18-
ERROR: principal key not configured
19-
HINT: Use pg_tde_set_key_using_database_key_provider() or pg_tde_set_key_using_global_key_provider() to configure one.
4+
-- FAILS as mount path does not exist
5+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'DUMMY-MOUNT-PATH', :'root_token_file', :'cacert_file');
6+
ERROR: failed to get mount info for "https://127.0.0.1:8200" at mountpoint "DUMMY-MOUNT-PATH" (HTTP 400)
7+
-- FAILS as it's not supported engine type
8+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'cubbyhole', :'root_token_file', :'cacert_file');
9+
ERROR: vault mount at "cubbyhole" has unsupported engine type "cubbyhole"
10+
HINT: The only supported vault engine type is Key/Value version "2"
11+
-- FAILS as it's not supported engine version
12+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'kv-v1', :'root_token_file', :'cacert_file');
13+
ERROR: vault mount at "kv-v1" has unsupported Key/Value engine version "1"
14+
HINT: The only supported vault engine type is Key/Value version "2"
2015
SELECT pg_tde_add_database_key_provider_vault_v2('vault-v2', 'https://127.0.0.1:8200', 'secret', :'root_token_file', :'cacert_file');
2116
pg_tde_add_database_key_provider_vault_v2
2217
-------------------------------------------
@@ -69,5 +64,5 @@ SELECT pg_tde_change_database_key_provider_vault_v2('vault-v2', 'https://127.0.0
6964
ERROR: HTTP(S) request to keyring provider "vault-v2" failed
7065
-- HTTP against HTTPS server fails
7166
SELECT pg_tde_change_database_key_provider_vault_v2('vault-v2', 'http://127.0.0.1:8200', 'secret', :'root_token_file', NULL);
72-
ERROR: Listing secrets of "http://127.0.0.1:8200" at mountpoint "secret" failed
67+
ERROR: failed to get mount info for "http://127.0.0.1:8200" at mountpoint "secret" (HTTP 400)
7368
DROP EXTENSION pg_tde;

contrib/pg_tde/sql/vault_v2_test.sql

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ CREATE EXTENSION pg_tde;
33
\getenv root_token_file VAULT_ROOT_TOKEN_FILE
44
\getenv cacert_file VAULT_CACERT_FILE
55

6-
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'DUMMY-TOKEN', :'root_token_file', :'cacert_file');
7-
-- FAILS
8-
SELECT pg_tde_create_key_using_database_key_provider('vault-v2-key', 'vault-incorrect');
6+
-- FAILS as mount path does not exist
7+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'DUMMY-MOUNT-PATH', :'root_token_file', :'cacert_file');
98

10-
CREATE TABLE test_enc(
11-
id SERIAL,
12-
k INTEGER DEFAULT '0' NOT NULL,
13-
PRIMARY KEY (id)
14-
) USING tde_heap;
9+
-- FAILS as it's not supported engine type
10+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'cubbyhole', :'root_token_file', :'cacert_file');
11+
12+
-- FAILS as it's not supported engine version
13+
SELECT pg_tde_add_database_key_provider_vault_v2('vault-incorrect', 'https://127.0.0.1:8200', 'kv-v1', :'root_token_file', :'cacert_file');
1514

1615
SELECT pg_tde_add_database_key_provider_vault_v2('vault-v2', 'https://127.0.0.1:8200', 'secret', :'root_token_file', :'cacert_file');
1716
SELECT pg_tde_create_key_using_database_key_provider('vault-v2-key', 'vault-v2');

contrib/pg_tde/src/keyring/keyring_vault.c

Lines changed: 241 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ typedef enum
3333
JRESP_EXPECT_KEY
3434
} JsonVaultRespSemState;
3535

36+
typedef enum
37+
{
38+
JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD,
39+
JRESP_MOUNT_INFO_EXPECT_TYPE_VALUE,
40+
JRESP_MOUNT_INFO_EXPECT_VERSION_VALUE,
41+
JRESP_MOUNT_INFO_EXPECT_OPTIONS_START,
42+
JRESP_MOUNT_INFO_EXPECT_OPTIONS_FIELD,
43+
} JsonVaultRespMountInfoSemState;
44+
45+
3646
typedef enum
3747
{
3848
JRESP_F_UNUSED,
@@ -49,12 +59,27 @@ typedef struct JsonVaultRespState
4959
char *key;
5060
} JsonVaultRespState;
5161

62+
typedef struct JsonVaultMountInfoState
63+
{
64+
JsonVaultRespMountInfoSemState state;
65+
int level;
66+
67+
char *type;
68+
char *version;
69+
} JsonVaultMountInfoState;
70+
5271
static JsonParseErrorType json_resp_object_start(void *state);
5372
static JsonParseErrorType json_resp_object_end(void *state);
5473
static JsonParseErrorType json_resp_scalar(void *state, char *token, JsonTokenType tokentype);
5574
static JsonParseErrorType json_resp_object_field_start(void *state, char *fname, bool isnull);
5675
static JsonParseErrorType parse_json_response(JsonVaultRespState *parse, JsonLexContext *lex);
5776

77+
static JsonParseErrorType json_mountinfo_object_start(void *state);
78+
static JsonParseErrorType json_mountinfo_object_end(void *state);
79+
static JsonParseErrorType json_mountinfo_scalar(void *state, char *token, JsonTokenType tokentype);
80+
static JsonParseErrorType json_mountinfo_object_field_start(void *state, char *fname, bool isnull);
81+
static JsonParseErrorType parse_vault_mount_info(JsonVaultMountInfoState *state, JsonLexContext *lex);
82+
5883
static char *get_keyring_vault_url(VaultV2Keyring *keyring, const char *key_name, char *out, size_t out_size);
5984
static bool curl_perform(VaultV2Keyring *keyring, const char *url, CurlString *outStr, long *httpCode, const char *postData);
6085

@@ -299,38 +324,99 @@ validate(GenericKeyring *keyring)
299324
{
300325
VaultV2Keyring *vault_keyring = (VaultV2Keyring *) keyring;
301326
char url[VAULT_URL_MAX_LEN];
327+
int len = 0;
302328
CurlString str;
303329
long httpCode = 0;
330+
JsonParseErrorType json_error;
331+
JsonLexContext *jlex = NULL;
332+
JsonVaultMountInfoState parse;
304333

305334
/*
306-
* Validate connection by listing available keys at the root level of the
307-
* mount point
335+
* Validate that the mount has the correct engine type and version.
308336
*/
309-
snprintf(url, VAULT_URL_MAX_LEN, "%s/v1/%s/metadata/?list=true",
310-
vault_keyring->vault_url, vault_keyring->vault_mount_path);
337+
len = snprintf(url, VAULT_URL_MAX_LEN, "%s/v1/sys/mounts/%s", vault_keyring->vault_url, vault_keyring->vault_mount_path);
338+
if (len >= VAULT_URL_MAX_LEN)
339+
ereport(ERROR,
340+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
341+
errmsg("vault mounts URL is too long"));
342+
343+
if (!curl_perform(vault_keyring, url, &str, &httpCode, NULL))
344+
ereport(ERROR,
345+
errmsg("HTTP(S) request to keyring provider \"%s\" failed",
346+
vault_keyring->keyring.provider_name));
347+
348+
if (httpCode != 200)
349+
ereport(ERROR,
350+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
351+
errmsg("failed to get mount info for \"%s\" at mountpoint \"%s\" (HTTP %ld)",
352+
vault_keyring->vault_url, vault_keyring->vault_mount_path, httpCode));
353+
354+
jlex = makeJsonLexContextCstringLen(NULL, str.ptr, str.len, PG_UTF8, true);
355+
json_error = parse_vault_mount_info(&parse, jlex);
356+
357+
if (json_error != JSON_SUCCESS)
358+
ereport(ERROR,
359+
errcode(ERRCODE_INVALID_JSON_TEXT),
360+
errmsg("failed to parse mount info for \"%s\" at mountpoint \"%s\": %s",
361+
vault_keyring->vault_url, vault_keyring->vault_mount_path, json_errdetail(json_error, jlex)));
362+
363+
if (parse.type == NULL)
364+
ereport(ERROR,
365+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
366+
errmsg("failed to parse mount info for \"%s\" at mountpoint \"%s\": missing type field",
367+
vault_keyring->vault_url, vault_keyring->vault_mount_path));
368+
369+
if (strcmp(parse.type, "kv") != 0)
370+
ereport(ERROR,
371+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
372+
errmsg("vault mount at \"%s\" has unsupported engine type \"%s\"",
373+
vault_keyring->vault_mount_path, parse.type),
374+
errhint("The only supported vault engine type is Key/Value version \"2\""));
375+
376+
if (parse.version == NULL)
377+
ereport(ERROR,
378+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
379+
errmsg("failed to parse mount info for \"%s\" at mountpoint \"%s\": missing version field",
380+
vault_keyring->vault_url, vault_keyring->vault_mount_path));
381+
382+
if (strcmp(parse.version, "2") != 0)
383+
ereport(ERROR,
384+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
385+
errmsg("vault mount at \"%s\" has unsupported Key/Value engine version \"%s\"",
386+
vault_keyring->vault_mount_path, parse.version),
387+
errhint("The only supported vault engine type is Key/Value version \"2\""));
388+
389+
/*
390+
* Validate that we can read the secrets at the mount point.
391+
*/
392+
len = snprintf(url, VAULT_URL_MAX_LEN, "%s/v1/%s/metadata/?list=true",
393+
vault_keyring->vault_url, vault_keyring->vault_mount_path);
394+
if (len >= VAULT_URL_MAX_LEN)
395+
ereport(ERROR,
396+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
397+
errmsg("vault metadata URL is too long"));
311398

312399
if (!curl_perform(vault_keyring, url, &str, &httpCode, NULL))
313-
{
314400
ereport(ERROR,
315401
errmsg("HTTP(S) request to keyring provider \"%s\" failed",
316402
vault_keyring->keyring.provider_name));
317-
}
318403

319404
/* If the mount point doesn't have any secrets yet, we'll get a 404. */
320405
if (httpCode != 200 && httpCode != 404)
321-
{
322406
ereport(ERROR,
323407
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
324408
errmsg("Listing secrets of \"%s\" at mountpoint \"%s\" failed",
325409
vault_keyring->vault_url, vault_keyring->vault_mount_path));
326-
}
327410

328411
if (str.ptr != NULL)
329412
pfree(str.ptr);
413+
414+
if (jlex != NULL)
415+
freeJsonLexContext(jlex);
330416
}
331417

332418
/*
333-
* JSON parser routines
419+
* JSON parser routines for key response
334420
*
335421
* We expect the response in the form of:
336422
* {
@@ -445,6 +531,152 @@ json_resp_object_field_start(void *state, char *fname, bool isnull)
445531
if (strcmp(fname, "key") == 0 && parse->level == 2)
446532
parse->field = JRESP_F_KEY;
447533
break;
534+
default:
535+
/* NOP */
536+
break;
537+
}
538+
539+
return JSON_SUCCESS;
540+
}
541+
542+
/*
543+
* JSON parser routines for mount info
544+
*
545+
* We expect the response in the form of:
546+
* {
547+
* ...
548+
* "type": "kv",
549+
* "options": {
550+
* "version": "2"
551+
* }
552+
* ...
553+
* }
554+
*
555+
* the rest fields are ignored
556+
*/
557+
558+
static JsonParseErrorType
559+
parse_vault_mount_info(JsonVaultMountInfoState *state, JsonLexContext *lex)
560+
{
561+
JsonSemAction sem;
562+
563+
state->state = JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD;
564+
state->type = NULL;
565+
state->version = NULL;
566+
state->level = -1;
567+
568+
memset(&sem, 0, sizeof(sem));
569+
sem.semstate = state;
570+
sem.object_start = json_mountinfo_object_start;
571+
sem.object_end = json_mountinfo_object_end;
572+
sem.scalar = json_mountinfo_scalar;
573+
sem.object_field_start = json_mountinfo_object_field_start;
574+
575+
return pg_parse_json(lex, &sem);
576+
}
577+
578+
static JsonParseErrorType
579+
json_mountinfo_object_start(void *state)
580+
{
581+
JsonVaultMountInfoState *parse = (JsonVaultMountInfoState *) state;
582+
583+
switch (parse->state)
584+
{
585+
case JRESP_MOUNT_INFO_EXPECT_OPTIONS_START:
586+
parse->state = JRESP_MOUNT_INFO_EXPECT_OPTIONS_FIELD;
587+
break;
588+
default:
589+
/* NOP */
590+
break;
591+
}
592+
593+
parse->level++;
594+
595+
return JSON_SUCCESS;
596+
}
597+
598+
static JsonParseErrorType
599+
json_mountinfo_object_end(void *state)
600+
{
601+
JsonVaultMountInfoState *parse = (JsonVaultMountInfoState *) state;
602+
603+
if (parse->state == JRESP_MOUNT_INFO_EXPECT_OPTIONS_FIELD)
604+
parse->state = JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD;
605+
606+
parse->level--;
607+
608+
return JSON_SUCCESS;
609+
}
610+
611+
static JsonParseErrorType
612+
json_mountinfo_scalar(void *state, char *token, JsonTokenType tokentype)
613+
{
614+
JsonVaultMountInfoState *parse = (JsonVaultMountInfoState *) state;
615+
616+
switch (parse->state)
617+
{
618+
case JRESP_MOUNT_INFO_EXPECT_TYPE_VALUE:
619+
parse->type = token;
620+
parse->state = JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD;
621+
break;
622+
case JRESP_MOUNT_INFO_EXPECT_VERSION_VALUE:
623+
parse->version = token;
624+
parse->state = JRESP_MOUNT_INFO_EXPECT_OPTIONS_FIELD;
625+
break;
626+
case JRESP_MOUNT_INFO_EXPECT_OPTIONS_START:
627+
628+
/*
629+
* Reset "options" object expectations if we got scalar. Most
630+
* likely just a null.
631+
*/
632+
parse->state = JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD;
633+
break;
634+
default:
635+
/* NOP */
636+
break;
637+
}
638+
639+
return JSON_SUCCESS;
640+
}
641+
642+
static JsonParseErrorType
643+
json_mountinfo_object_field_start(void *state, char *fname, bool isnull)
644+
{
645+
JsonVaultMountInfoState *parse = (JsonVaultMountInfoState *) state;
646+
647+
switch (parse->state)
648+
{
649+
case JRESP_MOUNT_INFO_EXPECT_TOPLEVEL_FIELD:
650+
if (parse->level == 0)
651+
{
652+
if (strcmp(fname, "type") == 0)
653+
{
654+
parse->state = JRESP_MOUNT_INFO_EXPECT_TYPE_VALUE;
655+
break;
656+
}
657+
658+
if (strcmp(fname, "options") == 0)
659+
{
660+
parse->state = JRESP_MOUNT_INFO_EXPECT_OPTIONS_START;
661+
break;
662+
}
663+
}
664+
break;
665+
666+
case JRESP_MOUNT_INFO_EXPECT_OPTIONS_FIELD:
667+
if (parse->level == 1)
668+
{
669+
if (strcmp(fname, "version") == 0)
670+
{
671+
parse->state = JRESP_MOUNT_INFO_EXPECT_VERSION_VALUE;
672+
break;
673+
}
674+
}
675+
break;
676+
677+
default:
678+
/* NOP */
679+
break;
448680
}
449681

450682
pfree(fname);

0 commit comments

Comments
 (0)