diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 387adb6da29c9c4776bd0060fdbecea75bc929b7..0928d3c06cd45bb2ad3235de07adcb6dc10cfdf8 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -337,6 +337,11 @@ echo 'actual-secret' | openssl rsautl -encrypt -pubin -inkey rsa_pub.pem | base6
 
 Replace `actual-secret` with the secret to encrypt.
 
+## privateKeyOld
+
+Use this field if you need to perform a "key rotation" and support more than one keypair at a time.
+Decryption with this key will be attempted after `privateKey`.
+
 ## privateKeyPath
 
 Used as an alternative to `privateKey`, if you wish for the key to be read from disk instead.
diff --git a/lib/config/decrypt.spec.ts b/lib/config/decrypt.spec.ts
index 7466e0a8cbc35a2ae3d97a9ab4adb53bca4f31e4..970c7814571ef3c08556adac9349c90ef66b5507 100644
--- a/lib/config/decrypt.spec.ts
+++ b/lib/config/decrypt.spec.ts
@@ -31,11 +31,11 @@ describe('config/decrypt', () => {
     });
     it('handles invalid encrypted value', () => {
       config.encrypted = { a: 1 };
-      setGlobalConfig({ privateKey });
+      setGlobalConfig({ privateKey, privateKeyOld: 'invalid-key' });
       expect(() => decryptConfig(config)).toThrow(Error('config-validation'));
     });
     it('replaces npm token placeholder in npmrc', () => {
-      setGlobalConfig({ privateKey });
+      setGlobalConfig({ privateKey: 'invalid-key', privateKeyOld: privateKey }); // test old key failover
       config.npmrc =
         '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n'; // eslint-disable-line no-template-curly-in-string
       config.encrypted = {
diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts
index e8848f2bb4fb6c042fab978ac21fed7bbc64a797..afabca0c6824a8e7dd2808483cf4fa27a80f8c5f 100644
--- a/lib/config/decrypt.ts
+++ b/lib/config/decrypt.ts
@@ -6,78 +6,105 @@ import { add } from '../util/sanitize';
 import { getGlobalConfig } from './global';
 import type { RenovateConfig } from './types';
 
+export function tryDecryptPublicKeyDefault(
+  privateKey: string,
+  encryptedStr: string
+): string | null {
+  let decryptedStr: string = null;
+  try {
+    decryptedStr = crypto
+      .privateDecrypt(privateKey, Buffer.from(encryptedStr, 'base64'))
+      .toString();
+    logger.debug('Decrypted config using default padding');
+  } catch (err) {
+    logger.debug('Failed to decrypt using default padding');
+  }
+  return decryptedStr;
+}
+
+export function tryDecryptPublicKeyPKCS1(
+  privateKey: string,
+  encryptedStr: string
+): string | null {
+  let decryptedStr: string = null;
+  try {
+    decryptedStr = crypto
+      .privateDecrypt(
+        {
+          key: privateKey,
+          padding: crypto.constants.RSA_PKCS1_PADDING,
+        },
+        Buffer.from(encryptedStr, 'base64')
+      )
+      .toString();
+  } catch (err) {
+    logger.debug('Failed to decrypt using PKCS1 padding');
+  }
+  return decryptedStr;
+}
+
+export function tryDecrypt(
+  privateKey: string,
+  encryptedStr: string
+): string | null {
+  let decryptedStr = tryDecryptPublicKeyDefault(privateKey, encryptedStr);
+  if (!is.string(decryptedStr)) {
+    decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr);
+  }
+  return decryptedStr;
+}
+
 export function decryptConfig(config: RenovateConfig): RenovateConfig {
   logger.trace({ config }, 'decryptConfig()');
   const decryptedConfig = { ...config };
-  const { privateKey } = getGlobalConfig();
+  const { privateKey, privateKeyOld } = getGlobalConfig();
   for (const [key, val] of Object.entries(config)) {
     if (key === 'encrypted' && is.object(val)) {
       logger.debug({ config: val }, 'Found encrypted config');
       if (privateKey) {
         for (const [eKey, eVal] of Object.entries(val)) {
-          try {
-            let decryptedStr: string;
-            try {
-              logger.debug('Trying default padding for ' + eKey);
-              decryptedStr = crypto
-                .privateDecrypt(privateKey, Buffer.from(eVal, 'base64'))
-                .toString();
-              logger.debug('Decrypted config using default padding');
-            } catch (err) {
-              logger.debug('Trying RSA_PKCS1_PADDING for ' + eKey);
-              decryptedStr = crypto
-                .privateDecrypt(
-                  {
-                    key: privateKey,
-                    padding: crypto.constants.RSA_PKCS1_PADDING,
-                  },
-                  Buffer.from(eVal, 'base64')
-                )
-                .toString();
-              // let it throw if the above fails
-            }
-            // istanbul ignore if
-            if (!decryptedStr.length) {
-              throw new Error('empty string');
-            }
-            logger.debug(`Decrypted ${eKey}`);
-            if (eKey === 'npmToken') {
-              const token = decryptedStr.replace(/\n$/, '');
-              add(token);
-              logger.debug(
-                { decryptedToken: maskToken(token) },
-                'Migrating npmToken to npmrc'
-              );
-              if (is.string(decryptedConfig.npmrc)) {
-                /* eslint-disable no-template-curly-in-string */
-                if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) {
-                  logger.debug('Replacing ${NPM_TOKEN} with decrypted token');
-                  decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
-                    /\${NPM_TOKEN}/g,
-                    token
-                  );
-                } else {
-                  logger.debug(
-                    'Appending _authToken= to end of existing npmrc'
-                  );
-                  decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
-                    /\n?$/,
-                    `\n_authToken=${token}\n`
-                  );
-                }
-                /* eslint-enable no-template-curly-in-string */
+          logger.debug('Trying to decrypt ' + eKey);
+          let decryptedStr = tryDecrypt(privateKey, eVal);
+          if (privateKeyOld && !is.nonEmptyString(decryptedStr)) {
+            logger.debug(`Trying to decrypt with old private key`);
+            decryptedStr = tryDecrypt(privateKeyOld, eVal);
+          }
+          if (!is.nonEmptyString(decryptedStr)) {
+            const error = new Error('config-validation');
+            error.validationError = `Failed to decrypt field ${eKey}. Please re-encrypt and try again.`;
+            throw error;
+          }
+          logger.debug(`Decrypted ${eKey}`);
+          if (eKey === 'npmToken') {
+            const token = decryptedStr.replace(/\n$/, '');
+            add(token);
+            logger.debug(
+              { decryptedToken: maskToken(token) },
+              'Migrating npmToken to npmrc'
+            );
+            if (is.string(decryptedConfig.npmrc)) {
+              /* eslint-disable no-template-curly-in-string */
+              if (decryptedConfig.npmrc.includes('${NPM_TOKEN}')) {
+                logger.debug('Replacing ${NPM_TOKEN} with decrypted token');
+                decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
+                  /\${NPM_TOKEN}/g,
+                  token
+                );
               } else {
-                logger.debug('Adding npmrc to config');
-                decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
+                logger.debug('Appending _authToken= to end of existing npmrc');
+                decryptedConfig.npmrc = decryptedConfig.npmrc.replace(
+                  /\n?$/,
+                  `\n_authToken=${token}\n`
+                );
               }
+              /* eslint-enable no-template-curly-in-string */
             } else {
-              decryptedConfig[eKey] = decryptedStr;
-              add(decryptedStr);
+              logger.debug('Adding npmrc to config');
+              decryptedConfig.npmrc = `//registry.npmjs.org/:_authToken=${token}\n`;
             }
-          } catch (err) {
-            const error = new Error('config-validation');
-            error.validationError = `Failed to decrypt field ${eKey}. Please re-encrypt and try again.`;
-            throw error;
+          } else {
+            decryptedConfig[eKey] = decryptedStr;
+            add(decryptedStr);
           }
         }
       } else {
diff --git a/lib/config/global.ts b/lib/config/global.ts
index 5b5e427d901b6e9053b414b0017fd3b4343e5004..0d17dd3992d23eb6a220df0132c2efd59e5b171f 100644
--- a/lib/config/global.ts
+++ b/lib/config/global.ts
@@ -17,6 +17,7 @@ const repoGlobalOptions = [
   'exposeAllEnv',
   'migratePresets',
   'privateKey',
+  'privateKeyOld',
   'localDir',
   'cacheDir',
 ];
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index c6f9eec3b9d5cf5ae66e6a1e125c9a2695a1edcd..45dcbc13f993ac30de034f09cbe8ef4efbbab6ad 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -452,6 +452,14 @@ const options: RenovateOptions[] = [
     replaceLineReturns: true,
     globalOnly: true,
   },
+  {
+    name: 'privateKeyOld',
+    description: 'Secondary/old private key to try.',
+    stage: 'repository',
+    type: 'string',
+    replaceLineReturns: true,
+    globalOnly: true,
+  },
   {
     name: 'privateKeyPath',
     description: 'Path to the Server-side private key.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 0e5fbdff9f02642af1c8d369f9d59c484d23dd79..ea13c4919112355d90b5aff158ce879fd72059e2 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -99,7 +99,8 @@ export interface RepoGlobalConfig {
   dryRun?: boolean;
   exposeAllEnv?: boolean;
   migratePresets?: Record<string, string>;
-  privateKey?: string | Buffer;
+  privateKey?: string;
+  privateKeyOld?: string;
   localDir?: string;
   cacheDir?: string;
 }
diff --git a/lib/util/sanitize.ts b/lib/util/sanitize.ts
index a169cd92eb8c30a7a74db32d778720360a7521bd..5f1a0f234e13c9788b959f5e84008710db546c59 100644
--- a/lib/util/sanitize.ts
+++ b/lib/util/sanitize.ts
@@ -7,6 +7,7 @@ export const redactedFields = [
   'npmToken',
   'npmrc',
   'privateKey',
+  'privateKeyOld',
   'gitPrivateKey',
   'forkToken',
   'password',
diff --git a/lib/workers/global/config/parse/index.spec.ts b/lib/workers/global/config/parse/index.spec.ts
index 6b8399e20511b9838401093155405bce93c942a1..066713775ba05cc6585a322352c9eaba9f074b6a 100644
--- a/lib/workers/global/config/parse/index.spec.ts
+++ b/lib/workers/global/config/parse/index.spec.ts
@@ -85,7 +85,7 @@ describe('workers/global/config/parse/index', () => {
         ...defaultEnv,
         RENOVATE_PRIVATE_KEY_PATH: privateKeyPath,
       };
-      const expected = await readFile(privateKeyPath);
+      const expected = await readFile(privateKeyPath, 'utf8');
       const parsedConfig = await configParser.parseConfigs(env, defaultArgv);
 
       expect(parsedConfig).toContainEntries([['privateKey', expected]]);
diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts
index ea32628c0e11611ef2d62ba2d41b5c2a6ea6a627..7fef39bc09cfe8ecfea2b88717e37000a138baef 100644
--- a/lib/workers/global/config/parse/index.ts
+++ b/lib/workers/global/config/parse/index.ts
@@ -39,7 +39,7 @@ export async function parseConfigs(
   }
 
   if (!config.privateKey && config.privateKeyPath) {
-    config.privateKey = await readFile(config.privateKeyPath);
+    config.privateKey = await readFile(config.privateKeyPath, 'utf8');
     delete config.privateKeyPath;
   }