-
Notifications
You must be signed in to change notification settings - Fork 8.9k
/
1711390882123-MoveSshKeysToDatabase.ts
111 lines (88 loc) · 3.26 KB
/
1711390882123-MoveSshKeysToDatabase.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import path from 'node:path';
import { readFile, writeFile, rm } from 'node:fs/promises';
import Container from 'typedi';
import { Cipher, InstanceSettings } from 'n8n-core';
import { jsonParse } from 'n8n-workflow';
import type { MigrationContext, ReversibleMigration } from '@db/types';
/**
* Move SSH key pair from file system to database, to enable SSH connections
* when running n8n in multiple containers - mains, webhooks, workers, etc.
*/
export class MoveSshKeysToDatabase1711390882123 implements ReversibleMigration {
private readonly settingsKey = 'features.sourceControl.sshKeys';
private readonly privateKeyPath = path.join(
Container.get(InstanceSettings).n8nFolder,
'ssh',
'key',
);
private readonly publicKeyPath = this.privateKeyPath + '.pub';
private readonly cipher = Container.get(Cipher);
async up({ escape, runQuery, logger, migrationName }: MigrationContext) {
let privateKey, publicKey;
try {
[privateKey, publicKey] = await Promise.all([
readFile(this.privateKeyPath, { encoding: 'utf8' }),
readFile(this.publicKeyPath, { encoding: 'utf8' }),
]);
} catch {
logger.info(`[${migrationName}] No SSH keys in filesystem, skipping`);
return;
}
const settings = escape.tableName('settings');
const rows: Array<{ value: string }> = await runQuery(
`SELECT value FROM ${settings} WHERE key = '${this.settingsKey}';`,
);
if (rows.length === 1) {
logger.info(`[${migrationName}] SSH keys already in database, skipping`);
return;
}
if (!privateKey) {
logger.error(`[${migrationName}] No private key found, skipping`);
return;
}
const value = JSON.stringify({
encryptedPrivateKey: this.cipher.encrypt(privateKey),
publicKey,
});
await runQuery(
`INSERT INTO ${settings} (key, value) VALUES ('${this.settingsKey}', '${value}');`,
);
try {
await Promise.all([rm(this.privateKeyPath), rm(this.publicKeyPath)]);
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
logger.error(
`[${migrationName}] Failed to remove SSH keys from filesystem: ${error.message}`,
);
}
}
async down({ escape, runQuery, logger, migrationName }: MigrationContext) {
const settings = escape.tableName('settings');
const rows: Array<{ value: string }> = await runQuery(
`SELECT value FROM ${settings} WHERE key = '${this.settingsKey}';`,
);
if (rows.length !== 1) {
logger.info(`[${migrationName}] No SSH keys in database, skipping revert`);
return;
}
const [row] = rows;
type KeyPair = { publicKey: string; encryptedPrivateKey: string };
const dbKeyPair = jsonParse<KeyPair | null>(row.value, { fallbackValue: null });
if (!dbKeyPair) {
logger.info(`[${migrationName}] Malformed SSH keys in database, skipping revert`);
return;
}
const privateKey = this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
const { publicKey } = dbKeyPair;
try {
await Promise.all([
writeFile(this.privateKeyPath, privateKey, { encoding: 'utf8', mode: 0o600 }),
writeFile(this.publicKeyPath, publicKey, { encoding: 'utf8', mode: 0o600 }),
]);
} catch {
logger.error(`[${migrationName}] Failed to write SSH keys to filesystem, skipping revert`);
return;
}
await runQuery(`DELETE ${settings} WHERE WHERE key = 'features.sourceControl.sshKeys';`);
}
}