Skip to content

Commit 4fcc844

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 44ff895 commit 4fcc844

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

@@ -290,38 +315,99 @@ validate(GenericKeyring *keyring)
290315
{
291316
VaultV2Keyring *vault_keyring = (VaultV2Keyring *) keyring;
292317
char url[VAULT_URL_MAX_LEN];
318+
int len = 0;
293319
CurlString str;
294320
long httpCode = 0;
321+
JsonParseErrorType json_error;
322+
JsonLexContext *jlex = NULL;
323+
JsonVaultMountInfoState parse;
295324

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

303390
if (!curl_perform(vault_keyring, url, &str, &httpCode, NULL))
304-
{
305391
ereport(ERROR,
306392
errmsg("HTTP(S) request to keyring provider \"%s\" failed",
307393
vault_keyring->keyring.provider_name));
308-
}
309394

310395
/* If the mount point doesn't have any secrets yet, we'll get a 404. */
311396
if (httpCode != 200 && httpCode != 404)
312-
{
313397
ereport(ERROR,
314398
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
315399
errmsg("Listing secrets of \"%s\" at mountpoint \"%s\" failed",
316400
vault_keyring->vault_url, vault_keyring->vault_mount_path));
317-
}
318401

319402
if (str.ptr != NULL)
320403
pfree(str.ptr);
404+
405+
if (jlex != NULL)
406+
freeJsonLexContext(jlex);
321407
}
322408

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

441673
return JSON_SUCCESS;

0 commit comments

Comments
 (0)