Skip to content

Commit 093dcef

Browse files
authored
fix(core): Stop relying on filesystem for SSH keys (#9217)
1 parent 3986356 commit 093dcef

File tree

2 files changed

+42
-49
lines changed

2 files changed

+42
-49
lines changed

packages/cli/src/environments/sourceControl/sourceControlGit.service.ee.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,26 @@ export class SourceControlGitService {
7979

8080
sourceControlFoldersExistCheck([gitFolder, sshFolder]);
8181

82+
await this.setGitSshCommand(gitFolder, sshFolder);
83+
84+
if (!(await this.checkRepositorySetup())) {
85+
await (this.git as unknown as SimpleGit).init();
86+
}
87+
if (!(await this.hasRemote(sourceControlPreferences.repositoryUrl))) {
88+
if (sourceControlPreferences.connected && sourceControlPreferences.repositoryUrl) {
89+
const instanceOwner = await this.ownershipService.getInstanceOwner();
90+
await this.initRepository(sourceControlPreferences, instanceOwner);
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Update the SSH command with the path to the temp file containing the private key from the DB.
97+
*/
98+
async setGitSshCommand(
99+
gitFolder = this.sourceControlPreferencesService.gitFolder,
100+
sshFolder = this.sourceControlPreferencesService.sshFolder,
101+
) {
82102
const privateKeyPath = await this.sourceControlPreferencesService.getPrivateKeyPath();
83103

84104
const sshKnownHosts = path.join(sshFolder, 'known_hosts');
@@ -94,21 +114,8 @@ export class SourceControlGitService {
94114
const { simpleGit } = await import('simple-git');
95115

96116
this.git = simpleGit(this.gitOptions)
97-
// Tell git not to ask for any information via the terminal like for
98-
// example the username. As nobody will be able to answer it would
99-
// n8n keep on waiting forever.
100117
.env('GIT_SSH_COMMAND', sshCommand)
101118
.env('GIT_TERMINAL_PROMPT', '0');
102-
103-
if (!(await this.checkRepositorySetup())) {
104-
await this.git.init();
105-
}
106-
if (!(await this.hasRemote(sourceControlPreferences.repositoryUrl))) {
107-
if (sourceControlPreferences.connected && sourceControlPreferences.repositoryUrl) {
108-
const instanceOwner = await this.ownershipService.getInstanceOwner();
109-
await this.initRepository(sourceControlPreferences, instanceOwner);
110-
}
111-
}
112119
}
113120

114121
resetService() {
@@ -273,13 +280,15 @@ export class SourceControlGitService {
273280
if (!this.git) {
274281
throw new ApplicationError('Git is not initialized (fetch)');
275282
}
283+
await this.setGitSshCommand();
276284
return await this.git.fetch();
277285
}
278286

279287
async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> {
280288
if (!this.git) {
281289
throw new ApplicationError('Git is not initialized (pull)');
282290
}
291+
await this.setGitSshCommand();
283292
const params = {};
284293
if (options.ffOnly) {
285294
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -298,6 +307,7 @@ export class SourceControlGitService {
298307
if (!this.git) {
299308
throw new ApplicationError('Git is not initialized ({)');
300309
}
310+
await this.setGitSshCommand();
301311
if (force) {
302312
return await this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']);
303313
}

packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
import os from 'node:os';
21
import { writeFile, chmod, readFile } from 'node:fs/promises';
32
import Container, { Service } from 'typedi';
43
import { SourceControlPreferences } from './types/sourceControlPreferences';
54
import type { ValidationError } from 'class-validator';
65
import { validate } from 'class-validator';
7-
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
8-
import {
9-
generateSshKeyPair,
10-
isSourceControlLicensed,
11-
sourceControlFoldersExistCheck,
12-
} from './sourceControlHelper.ee';
6+
import { rm as fsRm } from 'fs/promises';
7+
import { generateSshKeyPair, isSourceControlLicensed } from './sourceControlHelper.ee';
138
import { Cipher, InstanceSettings } from 'n8n-core';
149
import { ApplicationError, jsonParse } from 'n8n-workflow';
1510
import {
@@ -35,7 +30,7 @@ export class SourceControlPreferencesService {
3530
readonly gitFolder: string;
3631

3732
constructor(
38-
instanceSettings: InstanceSettings,
33+
private readonly instanceSettings: InstanceSettings,
3934
private readonly logger: Logger,
4035
private readonly cipher: Cipher,
4136
) {
@@ -82,33 +77,29 @@ export class SourceControlPreferencesService {
8277
private async getPrivateKeyFromDatabase() {
8378
const dbKeyPair = await this.getKeyPairFromDatabase();
8479

85-
if (!dbKeyPair) return null;
80+
if (!dbKeyPair) throw new ApplicationError('Failed to find key pair in database');
8681

8782
return this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
8883
}
8984

9085
private async getPublicKeyFromDatabase() {
9186
const dbKeyPair = await this.getKeyPairFromDatabase();
9287

93-
if (!dbKeyPair) return null;
88+
if (!dbKeyPair) throw new ApplicationError('Failed to find key pair in database');
9489

9590
return dbKeyPair.publicKey;
9691
}
9792

9893
async getPrivateKeyPath() {
9994
const dbPrivateKey = await this.getPrivateKeyFromDatabase();
10095

101-
if (dbPrivateKey) {
102-
const tempFilePath = path.join(os.tmpdir(), 'ssh_private_key_temp');
103-
104-
await writeFile(tempFilePath, dbPrivateKey);
96+
const tempFilePath = path.join(this.instanceSettings.n8nFolder, 'ssh_private_key_temp');
10597

106-
await chmod(tempFilePath, 0o600);
98+
await writeFile(tempFilePath, dbPrivateKey);
10799

108-
return tempFilePath;
109-
}
100+
await chmod(tempFilePath, 0o600);
110101

111-
return this.sshKeyName; // fall back to key in filesystem
102+
return tempFilePath;
112103
}
113104

114105
async getPublicKey() {
@@ -136,33 +127,16 @@ export class SourceControlPreferencesService {
136127
}
137128

138129
/**
139-
* Will generate an ed25519 key pair and save it to the database and the file system
140-
* Note: this will overwrite any existing key pair
130+
* Generate an SSH key pair and write it to the database, overwriting any existing key pair.
141131
*/
142132
async generateAndSaveKeyPair(keyPairType?: KeyPairType): Promise<SourceControlPreferences> {
143-
sourceControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
144133
if (!keyPairType) {
145134
keyPairType =
146135
this.getPreferences().keyGeneratorType ??
147136
(config.get('sourceControl.defaultKeyPairType') as KeyPairType) ??
148137
'ed25519';
149138
}
150139
const keyPair = await generateSshKeyPair(keyPairType);
151-
if (keyPair.publicKey && keyPair.privateKey) {
152-
try {
153-
await fsWriteFile(this.sshKeyName + '.pub', keyPair.publicKey, {
154-
encoding: 'utf8',
155-
mode: 0o666,
156-
});
157-
await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 });
158-
} catch (error) {
159-
throw new ApplicationError('Failed to save key pair to disk', { cause: error });
160-
}
161-
}
162-
// update preferences only after generating key pair to prevent endless loop
163-
if (keyPairType !== this.getPreferences().keyGeneratorType) {
164-
await this.setPreferences({ keyGeneratorType: keyPairType });
165-
}
166140

167141
try {
168142
await Container.get(SettingsRepository).save({
@@ -177,6 +151,11 @@ export class SourceControlPreferencesService {
177151
throw new ApplicationError('Failed to write key pair to database', { cause: error });
178152
}
179153

154+
// update preferences only after generating key pair to prevent endless loop
155+
if (keyPairType !== this.getPreferences().keyGeneratorType) {
156+
await this.setPreferences({ keyGeneratorType: keyPairType });
157+
}
158+
180159
return this.getPreferences();
181160
}
182161

@@ -223,6 +202,10 @@ export class SourceControlPreferencesService {
223202
preferences: Partial<SourceControlPreferences>,
224203
saveToDb = true,
225204
): Promise<SourceControlPreferences> {
205+
const noKeyPair = (await this.getKeyPairFromDatabase()) === null;
206+
207+
if (noKeyPair) await this.generateAndSaveKeyPair();
208+
226209
this.sourceControlPreferences = preferences;
227210
if (saveToDb) {
228211
const settingsValue = JSON.stringify(this._sourceControlPreferences);

0 commit comments

Comments
 (0)