1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00
Files
pgbackrest/src/command/restore/timeline.c
David Steele 6e28c0445f Add archive key rotation for repository format 6.
At format 6 the repository can hold more than one encryption key. This is used to rotate the archive key, which is currently the only bulk data key that never changes.

Keys are stored by id in the new CipherSpecMap, with one key marked current for encrypting new files. At format 6 the cipher section of an info file stores every key by id along with the digest it derives with, e.g. cipher-pass={"1":{"digest":"sha256","key":"..."}}, and archive.info also records the current key id in cipher-pass-current. Format 5 files keep their single key, which is loaded as the key with no id.

archive-push writes the id of the archive key it used into the header of each WAL file. The readers use that id to select the key: archive-get, verify, the restore timeline read, the backup archive copy, and repo-get. A pusher that loaded archive.info before a rotation keeps working since each file records the key it was encrypted with. The filter that writes the header now also works through a repository host.

stanza-create for format 6 creates the first archive key with id 1. stanza-upgrade from format 5 keeps the existing archive key as id 0 so WAL already archived stays readable, and adds a key at id 1 for new WAL.

The new repo-cipher-rotate option sets how old the current archive key may be before expire replaces it, with a default of 90 days and a minimum of 15. Expire adds the new key, makes it current, and records the time of the rotation in archive.info as cipher-pass-rotate. At format 5 the option has no effect and warns only when set explicitly.

Expire now saves archive.info on every run. Backup did this only to refresh the timestamps for object store lifecycle rules and no longer writes archive.info, so with expire-auto disabled the timestamps are refreshed only when expire runs.
2026-10-02 08:37:45 +02:00

293 lines
13 KiB
C

/***********************************************************************************************************************************
Timeline Management
***********************************************************************************************************************************/
#include <build.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "command/restore/timeline.h"
#include "common/format/cipherBlockFormat.h"
#include "common/log.h"
#include "config/config.h"
#include "postgres/interface.h"
#include "postgres/version.h"
#include "storage/helper.h"
/***********************************************************************************************************************************
Parse history file
***********************************************************************************************************************************/
typedef struct HistoryItem
{
unsigned int timeline; // Parent timeline
uint64_t lsn; // Boundary lsn
} HistoryItem;
static List *
historyParse(const String *const history)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STRING, history);
FUNCTION_LOG_END();
List *const result = lstNewP(sizeof(HistoryItem));
MEM_CONTEXT_TEMP_BEGIN()
{
const StringList *const historyList = strLstNewSplitZ(history, "\n");
for (unsigned int historyIdx = 0; historyIdx < strLstSize(historyList); historyIdx++)
{
const String *const line = strTrim(strLstGet(historyList, historyIdx));
// Skip empty lines
if (strSize(line) == 0)
continue;
// Skip comments. PostgreSQL does not write comments into history files but it does allow for them.
if (strBeginsWithZ(line, "#"))
continue;
// Split line
const StringList *const split = strLstNewSplitZ(line, "\t");
if (strLstSize(split) < 2)
THROW_FMT(FormatError, "invalid history line format '%s'", strZ(strLstGet(historyList, historyIdx)));
const HistoryItem historyItem =
{
.timeline = cvtZToUInt(strZ(strLstGet(split, 0))),
.lsn = pgLsnFromStr(strLstGet(split, 1)),
};
ASSERT(historyItem.lsn > 0);
ASSERT(historyItem.timeline > 0);
lstAdd(result, &historyItem);
}
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(LIST, result);
}
/***********************************************************************************************************************************
Load history file
***********************************************************************************************************************************/
static List *
historyLoad(
const Storage *const storageRepo, const String *const archiveId, const unsigned int timeline,
const CipherSpecMap *const cipherSpecArchive)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, storageRepo);
FUNCTION_LOG_PARAM(STRING, archiveId);
FUNCTION_LOG_PARAM(UINT, timeline);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecArchive);
FUNCTION_LOG_END();
List *result;
MEM_CONTEXT_TEMP_BEGIN()
{
const String *const historyFile = strNewFmt(STORAGE_REPO_ARCHIVE "/%s/%08X.history", strZ(archiveId), timeline);
StorageRead *const storageRead = storageNewReadP(storageRepo, historyFile);
cipherBlockFormatFilterGroupReadAddMap(ioReadFilterGroup(storageReadIo(storageRead)), cipherSpecArchive);
const Buffer *const history = storageGetP(storageRead);
TRY_BEGIN()
{
result = lstMove(historyParse(strNewBuf(history)), memContextPrior());
}
CATCH_ANY()
{
THROW_FMT(
FormatError, "invalid timeline '%X' at '%s': %s", timeline, strZ(storagePathP(storageRepo, historyFile)),
errorMessage());
}
TRY_END();
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(LIST, result);
}
/***********************************************************************************************************************************
Find latest timeline
***********************************************************************************************************************************/
static unsigned int
timelineLatest(const Storage *const storageRepo, const String *const archiveId, const unsigned int timelineCurrent)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, storageRepo);
FUNCTION_LOG_PARAM(STRING, archiveId);
FUNCTION_LOG_PARAM(UINT, timelineCurrent);
FUNCTION_LOG_END();
ASSERT(storageRepo != NULL);
unsigned int result = timelineCurrent;
MEM_CONTEXT_TEMP_BEGIN()
{
const StringList *const historyList = strLstSort(
storageListP(
storageRepo, strNewFmt(STORAGE_REPO_ARCHIVE "/%s", strZ(archiveId)), .expression = STRDEF("[0-F]{8}\\.history")),
sortOrderDesc);
if (!strLstEmpty(historyList))
{
const unsigned int timelineLatest = cvtZToUIntBase(strZ(strSubN(strLstGet(historyList, 0), 0, 8)), 16);
if (timelineLatest > timelineCurrent)
result = timelineLatest;
}
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(UINT, result);
}
/**********************************************************************************************************************************/
STRING_STATIC(TIMELINE_CURRENT_STR, "current");
STRING_STATIC(TIMELINE_LATEST_STR, "latest");
FN_EXTERN void
timelineVerify(
const Storage *const storageRepo, const String *const archiveId, const unsigned int pgVersion,
const unsigned int timelineBackup, const uint64_t lsnBackup, const String *timelineTargetStr, const unsigned int recoveryType,
const CipherSpecMap *const cipherSpecArchive)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, storageRepo);
FUNCTION_LOG_PARAM(STRING, archiveId);
FUNCTION_LOG_PARAM(UINT, pgVersion);
FUNCTION_LOG_PARAM(UINT, timelineBackup);
FUNCTION_LOG_PARAM(UINT64, lsnBackup);
FUNCTION_LOG_PARAM(STRING, timelineTargetStr);
FUNCTION_LOG_PARAM(UINT, recoveryType);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecArchive);
FUNCTION_LOG_END();
ASSERT(storageRepo != NULL);
ASSERT(archiveId != NULL);
ASSERT(pgVersion != 0);
ASSERT(timelineBackup > 0);
ASSERT(lsnBackup > 0);
// If timeline target is not specified then set based on the default by PostgreSQL version. Force timeline to current when the
// recovery type is immediate since no timeline switch will be needed but PostgreSQL will error if there is a problem with the
// latest timeline.
if (timelineTargetStr == NULL)
{
if (pgVersion <= PG_VERSION_11 || recoveryType == CFGOPTVAL_RESTORE_TYPE_IMMEDIATE)
timelineTargetStr = TIMELINE_CURRENT_STR;
else
timelineTargetStr = TIMELINE_LATEST_STR;
}
// If target timeline is not current then verify that the timeline is valid based on backup timeline and lsn. Following the
// current/backup timeline is guaranteed as long as the WAL is available -- no history file required.
if (!strEq(timelineTargetStr, TIMELINE_CURRENT_STR))
{
MEM_CONTEXT_TEMP_BEGIN()
{
// Determine timeline target in order to load the history file. If target timeline is latest scan the history files to
// find the most recent. If a target timeline is a number then parse it.
unsigned int timelineTarget;
if (strEq(timelineTargetStr, TIMELINE_LATEST_STR))
timelineTarget = timelineLatest(storageRepo, archiveId, timelineBackup);
else
{
TRY_BEGIN()
{
if (strBeginsWithZ(timelineTargetStr, "0x"))
timelineTarget = cvtZToUIntBase(strZ(timelineTargetStr) + 2, 16);
else
timelineTarget = cvtZToUInt(strZ(timelineTargetStr));
}
CATCH_ANY()
{
THROW_FMT(DbMismatchError, "invalid target timeline '%s'", strZ(timelineTargetStr));
}
TRY_END();
}
// Only proceed if target timeline is not the backup timeline
if (timelineTarget != timelineBackup)
{
// Search through the history for the target timeline to make sure it includes the backup timeline. This follows
// the logic in PostgreSQL's readTimeLineHistory() and tliOfPointInHistory() but is a bit simplified for our usage.
const List *const historyList = historyLoad(storageRepo, archiveId, timelineTarget, cipherSpecArchive);
unsigned int timelineFound = 0;
for (unsigned int historyIdx = 0; historyIdx < lstSize(historyList); historyIdx++)
{
const HistoryItem *const historyItem = lstGet(historyList, historyIdx);
// If backup timeline exists in the target timeline's history before the fork LSN then restore can proceed
if (historyItem->timeline == timelineBackup && lsnBackup < historyItem->lsn)
{
timelineFound = timelineBackup;
break;
}
}
// Error when the timeline was not found or not found in the expected range
if (timelineFound == 0)
{
uint64_t lsnFound = 0;
// Check if timeline exists but did not fork off when expected
for (unsigned int historyIdx = 0; historyIdx < lstSize(historyList); historyIdx++)
{
const HistoryItem *const historyItem = lstGet(historyList, historyIdx);
if (historyItem->timeline == timelineBackup)
{
timelineFound = timelineBackup;
lsnFound = historyItem->lsn;
}
}
// The timeline was found but it forked off earlier than the backup LSN. This covers the common case where a
// standby is promoted by accident, which creates a new timeline, and then the user tries to restore a backup
// from the original primary that was made after the new timeline was created. Since PostgreSQL >= 12 will
// always try to follow the latest timeline this restore will fail since there is no path to the target
// timeline.
if (timelineFound != 0)
{
ASSERT(lsnFound != 0);
THROW_FMT(
DbMismatchError,
"target timeline %X forked from backup timeline %X at %s which is before backup lsn of %s\n"
"HINT: was the target timeline created by accidentally promoting a standby?\n"
"HINT: was the target timeline created by testing a restore without --archive-mode=off?\n"
"HINT: was the backup made after the target timeline was created?",
timelineTarget, timelineBackup, strZ(pgLsnToStr(lsnFound)), strZ(pgLsnToStr(lsnBackup)));
}
// Else the backup timeline was not found at all. This less common case occurs when, for example, a restore from
// timeline 4 results in timeline 7 being created on promotion because 5 and 6 have already been created by
// prior promotions and then the user tries to restore a backup from timeline 5 with a target of timeline 7.
// There is no relationship between timeline 5 and 7 in this case at any LSN range.
else
{
THROW_FMT(
DbMismatchError,
"backup timeline %X, lsn %s is not in the history of target timeline %X\n"
"HINT: was the target timeline created by promoting from a timeline < latest?",
timelineBackup, strZ(pgLsnToStr(lsnBackup)), timelineTarget);
}
}
}
}
MEM_CONTEXT_TEMP_END();
}
FUNCTION_LOG_RETURN_VOID();
}