mirror of https://github.com/postgres/postgres
To support some common WAL archiving tools, e.g. PgBackRest, we implement an archive_command and a restore_command which can wrap any command and use pipe() to create fake file to either read from or write to. The restore command makes sure to write encrypted files if WAL encryption is enabled. It uses the fresh WAL key generated by the server on the current start which works fine because we then just let the first invocation of the restore command set the start LSN of the key. For e.g. PgBackRest you would have the following commands: archive_command = 'pg_tde_archive_decrypt %p pgbackrest --stanza=demo archive-push %p' restore_command = 'pg_tde_restore_encrypt %f %p pgbackrest --stanza=demo archive-get %f "%p"'pull/238/head
parent
80d515b322
commit
f6f1ae33b1
@ -0,0 +1,228 @@ |
||||
#include "postgres_fe.h" |
||||
|
||||
#include <fcntl.h> |
||||
#include <sys/wait.h> |
||||
#include <unistd.h> |
||||
|
||||
#include "access/xlog_internal.h" |
||||
#include "access/xlog_smgr.h" |
||||
#include "common/logging.h" |
||||
|
||||
#include "access/pg_tde_fe_init.h" |
||||
#include "access/pg_tde_xlog_smgr.h" |
||||
|
||||
static bool |
||||
is_segment(const char *filename) |
||||
{ |
||||
return strspn(filename, "0123456789ABCDEF") == 24 && filename[24] == '\0'; |
||||
} |
||||
|
||||
static void |
||||
write_decrypted_segment(const char *segpath, const char *segname, int pipewr) |
||||
{ |
||||
int fd; |
||||
off_t fsize; |
||||
int r; |
||||
int w; |
||||
TimeLineID tli; |
||||
XLogSegNo segno; |
||||
PGAlignedXLogBlock buf; |
||||
off_t pos = 0; |
||||
|
||||
fd = open(segpath, O_RDONLY | PG_BINARY, 0); |
||||
if (fd < 0) |
||||
pg_fatal("could not open file \"%s\": %m", segname); |
||||
|
||||
/*
|
||||
* WalSegSz extracted from the first page header but it might be |
||||
* encrypted. But we need to know the segment seize to decrypt it (it's |
||||
* required for encryption offset calculations). So we get the segment |
||||
* size from the file's actual size. XLogLongPageHeaderData->xlp_seg_size |
||||
* there is "just as a cross-check" anyway. |
||||
*/ |
||||
fsize = lseek(fd, 0, SEEK_END); |
||||
XLogFromFileName(segname, &tli, &segno, fsize); |
||||
|
||||
r = xlog_smgr->seg_read(fd, buf.data, XLOG_BLCKSZ, pos, tli, segno, fsize); |
||||
|
||||
if (r == XLOG_BLCKSZ) |
||||
{ |
||||
XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data; |
||||
int walsegsz = longhdr->xlp_seg_size; |
||||
|
||||
if (walsegsz != fsize) |
||||
pg_fatal("mismatch of segment size in WAL file \"%s\" (header: %d bytes, file size: %ld bytes)", |
||||
segname, walsegsz, fsize); |
||||
|
||||
if (!IsValidWalSegSize(walsegsz)) |
||||
{ |
||||
pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%d byte)", |
||||
"invalid WAL segment size in WAL file \"%s\" (%d bytes)", |
||||
walsegsz), |
||||
segname, walsegsz); |
||||
pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB."); |
||||
exit(1); |
||||
} |
||||
} |
||||
else if (r < 0) |
||||
pg_fatal("could not read file \"%s\": %m", |
||||
segpath); |
||||
else |
||||
pg_fatal("could not read file \"%s\": read %d of %d", |
||||
segpath, r, XLOG_BLCKSZ); |
||||
|
||||
pos += r; |
||||
|
||||
w = write(pipewr, buf.data, XLOG_BLCKSZ); |
||||
|
||||
if (w < 0) |
||||
pg_fatal("could not write to pipe: %m"); |
||||
else if (w != r) |
||||
pg_fatal("could not write to pipe: wrote %d of %d", w, r); |
||||
|
||||
while (1) |
||||
{ |
||||
r = xlog_smgr->seg_read(fd, buf.data, XLOG_BLCKSZ, pos, tli, segno, fsize); |
||||
|
||||
if (r == 0) |
||||
break; |
||||
else if (r < 0) |
||||
pg_fatal("could not read file \"%s\": %m", segpath); |
||||
|
||||
pos += r; |
||||
|
||||
w = write(pipewr, buf.data, r); |
||||
|
||||
if (w < 0) |
||||
pg_fatal("could not write to pipe: %m"); |
||||
else if (w != r) |
||||
pg_fatal("could not write to pipe: wrote %d of %d", w, r); |
||||
} |
||||
|
||||
close(fd); |
||||
} |
||||
|
||||
static void |
||||
usage(const char *progname) |
||||
{ |
||||
printf(_("%s wraps an archive command to make it archive unencrypted WAL.\n\n"), progname); |
||||
printf(_("Usage:\n %s %%p <archive command>\n\n"), progname); |
||||
printf(_("Options:\n")); |
||||
printf(_(" -V, --version output version information, then exit\n")); |
||||
printf(_(" -?, --help show this help, then exit\n")); |
||||
} |
||||
|
||||
int |
||||
main(int argc, char *argv[]) |
||||
{ |
||||
const char *progname; |
||||
char *sourcepath; |
||||
char *sep; |
||||
char *sourcename; |
||||
char stdindir[MAXPGPATH] = "/tmp/pg_tde_archiveXXXXXX"; |
||||
char stdinpath[MAXPGPATH]; |
||||
bool issegment; |
||||
int pipefd[2]; |
||||
pid_t child; |
||||
int status; |
||||
int r; |
||||
|
||||
pg_logging_init(argv[0]); |
||||
progname = get_progname(argv[0]); |
||||
|
||||
if (argc > 1) |
||||
{ |
||||
if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) |
||||
{ |
||||
usage(progname); |
||||
exit(0); |
||||
} |
||||
if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0) |
||||
{ |
||||
puts("pg_tde_archive_deceypt (PostgreSQL) " PG_VERSION); |
||||
exit(0); |
||||
} |
||||
} |
||||
|
||||
if (argc < 3) |
||||
{ |
||||
pg_log_error("too few arguments"); |
||||
pg_log_error_detail("Try \"%s --help\" for more information.", progname); |
||||
exit(1); |
||||
} |
||||
|
||||
sourcepath = argv[1]; |
||||
|
||||
pg_tde_fe_init("pg_tde"); |
||||
TDEXLogSmgrInit(); |
||||
|
||||
sep = strrchr(sourcepath, '/'); |
||||
|
||||
if (sep != NULL) |
||||
sourcename = sep + 1; |
||||
else |
||||
sourcename = sourcepath; |
||||
|
||||
issegment = is_segment(sourcename); |
||||
|
||||
if (issegment) |
||||
{ |
||||
char *s; |
||||
|
||||
if (mkdtemp(stdindir) == NULL) |
||||
pg_fatal("could not create temporary directory \"%s\": %m", stdindir); |
||||
|
||||
s = stpcpy(stdinpath, stdindir); |
||||
s = stpcpy(s, "/"); |
||||
stpcpy(s, sourcename); |
||||
|
||||
if (pipe(pipefd) < 0) |
||||
pg_fatal("could not create pipe: %m"); |
||||
|
||||
if (symlink("/dev/stdin", stdinpath) < 0) |
||||
pg_fatal("could not create symlink \"%s\": %m", stdinpath); |
||||
|
||||
for (int i = 2; i < argc; i++) |
||||
if (strcmp(sourcepath, argv[i]) == 0) |
||||
argv[i] = stdinpath; |
||||
} |
||||
|
||||
child = fork(); |
||||
if (child == 0) |
||||
{ |
||||
if (issegment) |
||||
{ |
||||
close(0); |
||||
dup2(pipefd[0], 0); |
||||
close(pipefd[0]); |
||||
close(pipefd[1]); |
||||
} |
||||
|
||||
if (execvp(argv[2], argv + 2) < 0) |
||||
pg_fatal("exec failed: %m"); |
||||
} |
||||
else if (child < 0) |
||||
pg_fatal("could not create background process: %m"); |
||||
|
||||
if (issegment) |
||||
{ |
||||
close(pipefd[0]); |
||||
write_decrypted_segment(sourcepath, sourcename, pipefd[1]); |
||||
close(pipefd[1]); |
||||
} |
||||
|
||||
r = waitpid(child, &status, 0); |
||||
if (r == (pid_t) -1) |
||||
pg_fatal("could not wait for child process: %m"); |
||||
if (r != child) |
||||
pg_fatal("child %d died, expected %d", (int) r, (int) child); |
||||
if (status != 0) |
||||
pg_fatal("%s", wait_result_to_str(status)); |
||||
|
||||
if (issegment && unlink(stdinpath) < 0) |
||||
pg_log_warning("could not remove symlink \"%s\": %m", stdinpath); |
||||
if (issegment && rmdir(stdindir) < 0) |
||||
pg_log_warning("could not remove directory \"%s\": %m", stdindir); |
||||
|
||||
return 0; |
||||
} |
@ -0,0 +1,220 @@ |
||||
#include "postgres_fe.h" |
||||
|
||||
#include <fcntl.h> |
||||
#include <sys/wait.h> |
||||
#include <unistd.h> |
||||
|
||||
#include "access/xlog_internal.h" |
||||
#include "access/xlog_smgr.h" |
||||
#include "common/logging.h" |
||||
|
||||
#include "access/pg_tde_fe_init.h" |
||||
#include "access/pg_tde_xlog_smgr.h" |
||||
|
||||
static bool |
||||
is_segment(const char *filename) |
||||
{ |
||||
return strspn(filename, "0123456789ABCDEF") == 24 && filename[24] == '\0'; |
||||
} |
||||
|
||||
static void |
||||
write_encrypted_segment(const char *segpath, const char *segname, int piperd) |
||||
{ |
||||
int fd; |
||||
PGAlignedXLogBlock buf; |
||||
int r; |
||||
int w; |
||||
int pos = 0; |
||||
XLogLongPageHeader longhdr; |
||||
int walsegsz; |
||||
TimeLineID tli; |
||||
XLogSegNo segno; |
||||
|
||||
fd = open(segpath, O_CREAT | O_WRONLY | PG_BINARY, 0666); |
||||
if (fd < 0) |
||||
pg_fatal("could not open file \"%s\": %m", segpath); |
||||
|
||||
r = read(piperd, buf.data, XLOG_BLCKSZ); |
||||
|
||||
if (r < 0) |
||||
pg_fatal("could not read from pipe: %m"); |
||||
else if (r != XLOG_BLCKSZ) |
||||
pg_fatal("could not read from pipe: read %d of %d", |
||||
r, XLOG_BLCKSZ); |
||||
|
||||
longhdr = (XLogLongPageHeader) buf.data; |
||||
walsegsz = longhdr->xlp_seg_size; |
||||
|
||||
if (!IsValidWalSegSize(walsegsz)) |
||||
{ |
||||
pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%d byte)", |
||||
"invalid WAL segment size in WAL file \"%s\" (%d bytes)", |
||||
walsegsz), |
||||
segname, walsegsz); |
||||
pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB."); |
||||
exit(1); |
||||
} |
||||
|
||||
XLogFromFileName(segname, &tli, &segno, walsegsz); |
||||
|
||||
TDEXLogSmgrInitWriteReuseKey(); |
||||
|
||||
w = xlog_smgr->seg_write(fd, buf.data, r, pos, tli, segno, walsegsz); |
||||
|
||||
if (w < 0) |
||||
pg_fatal("could not write file \"%s\": %m", segpath); |
||||
else if (w != r) |
||||
pg_fatal("could not write file \"%s\": wrote %d of %d", |
||||
segpath, w, r); |
||||
|
||||
pos += w; |
||||
|
||||
while (1) |
||||
{ |
||||
r = read(piperd, buf.data, XLOG_BLCKSZ); |
||||
|
||||
if (r == 0) |
||||
break; |
||||
else if (r < 0) |
||||
pg_fatal("could not read from pipe: %m"); |
||||
|
||||
w = xlog_smgr->seg_write(fd, buf.data, r, pos, tli, segno, walsegsz); |
||||
|
||||
if (w < 0) |
||||
pg_fatal("could not write file \"%s\": %m", segpath); |
||||
else if (w != r) |
||||
pg_fatal("could not write file \"%s\": wrote %d of %d", |
||||
segpath, w, r); |
||||
|
||||
pos += w; |
||||
} |
||||
|
||||
close(fd); |
||||
} |
||||
|
||||
static void |
||||
usage(const char *progname) |
||||
{ |
||||
printf(_("%s wraps a restore command to make it write encrypted WAL to pg_wal.\n\n"), progname); |
||||
printf(_("Usage:\n %s %%f %%p <restore command>\n\n"), progname); |
||||
printf(_("Options:\n")); |
||||
printf(_(" -V, --version output version information, then exit\n")); |
||||
printf(_(" -?, --help show this help, then exit\n")); |
||||
} |
||||
|
||||
int |
||||
main(int argc, char *argv[]) |
||||
{ |
||||
const char *progname; |
||||
char *sourcename; |
||||
char *targetpath; |
||||
char *sep; |
||||
char *targetname; |
||||
char stdoutdir[MAXPGPATH] = "/tmp/pg_tde_restoreXXXXXX"; |
||||
char stdoutpath[MAXPGPATH]; |
||||
bool issegment; |
||||
int pipefd[2]; |
||||
pid_t child; |
||||
int status; |
||||
int r; |
||||
|
||||
pg_logging_init(argv[0]); |
||||
progname = get_progname(argv[0]); |
||||
|
||||
if (argc > 1) |
||||
{ |
||||
if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) |
||||
{ |
||||
usage(progname); |
||||
exit(0); |
||||
} |
||||
if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0) |
||||
{ |
||||
puts("pg_tde_restore_encrypt (PostgreSQL) " PG_VERSION); |
||||
exit(0); |
||||
} |
||||
} |
||||
|
||||
if (argc < 4) |
||||
{ |
||||
pg_log_error("too few arguments"); |
||||
pg_log_error_detail("Try \"%s --help\" for more information.", progname); |
||||
exit(1); |
||||
} |
||||
|
||||
sourcename = argv[1]; |
||||
targetpath = argv[2]; |
||||
|
||||
pg_tde_fe_init("pg_tde"); |
||||
TDEXLogSmgrInit(); |
||||
|
||||
sep = strrchr(targetpath, '/'); |
||||
|
||||
if (sep != NULL) |
||||
targetname = sep + 1; |
||||
else |
||||
targetname = targetpath; |
||||
|
||||
issegment = is_segment(sourcename); |
||||
|
||||
if (issegment) |
||||
{ |
||||
char *s; |
||||
|
||||
if (mkdtemp(stdoutdir) == NULL) |
||||
pg_fatal("could not create temporary directory \"%s\": %m", stdoutdir); |
||||
|
||||
s = stpcpy(stdoutpath, stdoutdir); |
||||
s = stpcpy(s, "/"); |
||||
stpcpy(s, targetname); |
||||
|
||||
if (pipe(pipefd) < 0) |
||||
pg_fatal("could not create pipe: %m"); |
||||
|
||||
if (symlink("/dev/stdout", stdoutpath) < 0) |
||||
pg_fatal("could not create symlink \"%s\": %m", stdoutpath); |
||||
|
||||
for (int i = 2; i < argc; i++) |
||||
if (strcmp(targetpath, argv[i]) == 0) |
||||
argv[i] = stdoutpath; |
||||
} |
||||
|
||||
child = fork(); |
||||
if (child == 0) |
||||
{ |
||||
if (issegment) |
||||
{ |
||||
close(1); |
||||
dup2(pipefd[1], 1); |
||||
close(pipefd[0]); |
||||
close(pipefd[1]); |
||||
} |
||||
|
||||
if (execvp(argv[3], argv + 3) < 0) |
||||
pg_fatal("exec failed: %m"); |
||||
} |
||||
else if (child < 0) |
||||
pg_fatal("could not create background process: %m"); |
||||
|
||||
if (issegment) |
||||
{ |
||||
close(pipefd[1]); |
||||
write_encrypted_segment(targetpath, sourcename, pipefd[0]); |
||||
close(pipefd[0]); |
||||
} |
||||
|
||||
r = waitpid(child, &status, 0); |
||||
if (r == (pid_t) -1) |
||||
pg_fatal("could not wait for child process: %m"); |
||||
if (r != child) |
||||
pg_fatal("child %d died, expected %d", (int) r, (int) child); |
||||
if (status != 0) |
||||
pg_fatal("%s", wait_result_to_str(status)); |
||||
|
||||
if (issegment && unlink(stdoutpath) < 0) |
||||
pg_log_warning("could not remove symlink \"%s\": %m", stdoutpath); |
||||
if (issegment && rmdir(stdoutdir) < 0) |
||||
pg_log_warning("could not remove directory \"%s\": %m", stdoutdir); |
||||
|
||||
return 0; |
||||
} |
@ -0,0 +1,101 @@ |
||||
#!/usr/bin/perl |
||||
|
||||
use strict; |
||||
use warnings; |
||||
use File::Basename; |
||||
use Test::More; |
||||
use lib 't'; |
||||
use pgtde; |
||||
|
||||
unlink('/tmp/wal_archiving.per'); |
||||
|
||||
# Test archive_command |
||||
|
||||
my $primary = PostgreSQL::Test::Cluster->new('primary'); |
||||
my $archive_dir = $primary->archive_dir; |
||||
$primary->init(allows_streaming => 1); |
||||
$primary->append_conf('postgresql.conf', |
||||
"shared_preload_libraries = 'pg_tde'"); |
||||
$primary->append_conf('postgresql.conf', "wal_level = 'replica'"); |
||||
$primary->append_conf('postgresql.conf', "autovacuum = off"); |
||||
$primary->append_conf('postgresql.conf', "checkpoint_timeout = 1h"); |
||||
$primary->append_conf('postgresql.conf', "archive_mode = on"); |
||||
$primary->append_conf('postgresql.conf', |
||||
"archive_command = 'pg_tde_archive_decrypt %p cp %p $archive_dir/%f'"); |
||||
$primary->start; |
||||
|
||||
$primary->safe_psql('postgres', "CREATE EXTENSION pg_tde;"); |
||||
|
||||
$primary->safe_psql('postgres', |
||||
"SELECT pg_tde_add_global_key_provider_file('keyring', '/tmp/wal_archiving.per');" |
||||
); |
||||
$primary->safe_psql('postgres', |
||||
"SELECT pg_tde_create_key_using_global_key_provider('server-key', 'keyring');" |
||||
); |
||||
$primary->safe_psql('postgres', |
||||
"SELECT pg_tde_set_server_key_using_global_key_provider('server-key', 'keyring');" |
||||
); |
||||
|
||||
$primary->append_conf('postgresql.conf', "pg_tde.wal_encrypt = on"); |
||||
|
||||
$primary->backup('backup', backup_options => [ '-X', 'none' ]); |
||||
|
||||
$primary->safe_psql('postgres', |
||||
"CREATE TABLE t1 AS SELECT 'foobar_plain' AS x"); |
||||
|
||||
$primary->restart; |
||||
|
||||
$primary->safe_psql('postgres', |
||||
"CREATE TABLE t2 AS SELECT 'foobar_enc' AS x"); |
||||
|
||||
my $data_dir = $primary->data_dir; |
||||
|
||||
like( |
||||
`strings $data_dir/pg_wal/0000000100000000000000??`, |
||||
qr/foobar_plain/, |
||||
'should find foobar_plain in WAL'); |
||||
unlike( |
||||
`strings $data_dir/pg_wal/0000000100000000000000??`, |
||||
qr/foobar_enc/, |
||||
'should not find foobar_enc in WAL'); |
||||
|
||||
$primary->stop; |
||||
|
||||
like( |
||||
`strings $archive_dir/0000000100000000000000??`, |
||||
qr/foobar_plain/, |
||||
'should find foobar_plain in archive'); |
||||
like( |
||||
`strings $archive_dir/0000000100000000000000??`, |
||||
qr/foobar_enc/, |
||||
'should find foobar_enc in archive'); |
||||
|
||||
# Test restore_command |
||||
|
||||
my $replica = PostgreSQL::Test::Cluster->new('replica'); |
||||
$replica->init_from_backup($primary, 'backup'); |
||||
$replica->append_conf('postgresql.conf', |
||||
"restore_command = 'pg_tde_restore_encrypt %f %p cp $archive_dir/%f %p'"); |
||||
$replica->append_conf('postgresql.conf', "recovery_target_action = promote"); |
||||
$replica->set_recovery_mode; |
||||
$replica->start; |
||||
|
||||
$data_dir = $replica->data_dir; |
||||
|
||||
unlike( |
||||
`strings $data_dir/pg_wal/0000000100000000000000??`, |
||||
qr/foobar_plain/, |
||||
'should not find foobar_plain in WAL since it is encrypted'); |
||||
unlike( |
||||
`strings $data_dir/pg_wal/0000000100000000000000??`, |
||||
qr/foobar_enc/, |
||||
'should not find foobar_enc in WAL since it is encrypted'); |
||||
|
||||
my $result = $replica->safe_psql('postgres', |
||||
'SELECT * FROM t1 UNION ALL SELECT * FROM t2'); |
||||
|
||||
is($result, "foobar_plain\nfoobar_enc", 'b'); |
||||
|
||||
$replica->stop; |
||||
|
||||
done_testing(); |
Loading…
Reference in new issue