@lamalibre/portlama-agent 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/setup.js +31 -62
- package/src/lib/cert-store.js +19 -52
- package/src/lib/keychain.js +24 -8
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -7,7 +7,6 @@ import { Listr } from 'listr2';
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import {
|
|
9
9
|
assertSupportedPlatform,
|
|
10
|
-
isDarwin,
|
|
11
10
|
CHISEL_BIN_DIR,
|
|
12
11
|
AGENT_DIR,
|
|
13
12
|
agentDataDir,
|
|
@@ -161,9 +160,7 @@ async function runTokenSetup(flags) {
|
|
|
161
160
|
|
|
162
161
|
console.log('');
|
|
163
162
|
console.log(chalk.bold(' Portlama Agent Setup (Token-Based Enrollment)'));
|
|
164
|
-
console.log(chalk.dim(
|
|
165
|
-
? ' Connect this Mac to your Portlama server using a Keychain-bound certificate.'
|
|
166
|
-
: ' Connect this machine to your Portlama server using a certificate.'));
|
|
163
|
+
console.log(chalk.dim(' Connect this machine to your Portlama server using a certificate.'));
|
|
167
164
|
console.log('');
|
|
168
165
|
|
|
169
166
|
let panelUrl = flags.panelUrl;
|
|
@@ -185,7 +182,6 @@ async function runTokenSetup(flags) {
|
|
|
185
182
|
explicitLabel: flags.label,
|
|
186
183
|
agentLabel: null,
|
|
187
184
|
resolvedLabel: null,
|
|
188
|
-
keychainIdentity: null,
|
|
189
185
|
p12Path: null,
|
|
190
186
|
p12Password: null,
|
|
191
187
|
chiselVersion: null,
|
|
@@ -247,7 +243,7 @@ async function runTokenSetup(flags) {
|
|
|
247
243
|
},
|
|
248
244
|
},
|
|
249
245
|
{
|
|
250
|
-
title:
|
|
246
|
+
title: 'Storing certificate',
|
|
251
247
|
task: async (_ctx, task) => {
|
|
252
248
|
const result = await storeEnrolledCert(
|
|
253
249
|
ctx._keyData.keyPath,
|
|
@@ -256,14 +252,9 @@ async function runTokenSetup(flags) {
|
|
|
256
252
|
ctx.resolvedLabel,
|
|
257
253
|
console,
|
|
258
254
|
);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} else {
|
|
263
|
-
ctx.p12Path = result.p12Path;
|
|
264
|
-
ctx.p12Password = result.p12Password;
|
|
265
|
-
task.output = `Certificate stored at ${result.p12Path}`;
|
|
266
|
-
}
|
|
255
|
+
ctx.p12Path = result.p12Path;
|
|
256
|
+
ctx.p12Password = result.p12Password;
|
|
257
|
+
task.output = `Certificate stored at ${result.p12Path}`;
|
|
267
258
|
},
|
|
268
259
|
rendererOptions: { persistentOutput: true },
|
|
269
260
|
},
|
|
@@ -277,9 +268,7 @@ async function runTokenSetup(flags) {
|
|
|
277
268
|
{
|
|
278
269
|
title: 'Verifying panel connectivity',
|
|
279
270
|
task: async (_ctx, task) => {
|
|
280
|
-
const authConfig = ctx.
|
|
281
|
-
? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
|
|
282
|
-
: { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
271
|
+
const authConfig = { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
283
272
|
const health = await fetchHealth(authConfig);
|
|
284
273
|
task.output = `Panel is reachable (status: ${health.status || 'ok'})`;
|
|
285
274
|
},
|
|
@@ -301,9 +290,7 @@ async function runTokenSetup(flags) {
|
|
|
301
290
|
{
|
|
302
291
|
title: 'Fetching tunnel configuration',
|
|
303
292
|
task: async (_ctx, task) => {
|
|
304
|
-
const authConfig = ctx.
|
|
305
|
-
? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
|
|
306
|
-
: { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
293
|
+
const authConfig = { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
307
294
|
|
|
308
295
|
const agentConfig = await fetchAgentConfig(authConfig);
|
|
309
296
|
ctx.domain = agentConfig.domain;
|
|
@@ -362,30 +349,24 @@ async function runTokenSetup(flags) {
|
|
|
362
349
|
task: async () => {
|
|
363
350
|
const configData = {
|
|
364
351
|
panelUrl: ctx.panelUrl,
|
|
352
|
+
authMethod: 'p12',
|
|
353
|
+
p12Path: ctx.p12Path,
|
|
354
|
+
p12Password: ctx.p12Password,
|
|
365
355
|
agentLabel: ctx.agentLabel,
|
|
366
356
|
domain: ctx.domain,
|
|
367
357
|
chiselVersion: ctx.chiselVersion,
|
|
368
358
|
setupAt: new Date().toISOString(),
|
|
369
359
|
};
|
|
370
360
|
|
|
371
|
-
if (ctx.keychainIdentity) {
|
|
372
|
-
configData.authMethod = 'keychain';
|
|
373
|
-
configData.keychainIdentity = ctx.keychainIdentity;
|
|
374
|
-
} else {
|
|
375
|
-
configData.authMethod = 'p12';
|
|
376
|
-
configData.p12Path = ctx.p12Path;
|
|
377
|
-
configData.p12Password = ctx.p12Password;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
361
|
await saveAgentConfig(ctx.resolvedLabel, configData);
|
|
381
362
|
|
|
382
363
|
// Add or update registry entry
|
|
383
364
|
await upsertAgent({
|
|
384
365
|
label: ctx.resolvedLabel,
|
|
385
366
|
panelUrl: ctx.panelUrl,
|
|
386
|
-
authMethod:
|
|
387
|
-
p12Path:
|
|
388
|
-
keychainIdentity:
|
|
367
|
+
authMethod: 'p12',
|
|
368
|
+
p12Path: ctx.p12Path,
|
|
369
|
+
keychainIdentity: null,
|
|
389
370
|
agentLabel: ctx.agentLabel,
|
|
390
371
|
domain: ctx.domain,
|
|
391
372
|
chiselVersion: ctx.chiselVersion,
|
|
@@ -440,7 +421,6 @@ async function runTokenSetupJson(flags) {
|
|
|
440
421
|
explicitLabel: flags.label,
|
|
441
422
|
agentLabel: null,
|
|
442
423
|
resolvedLabel: null,
|
|
443
|
-
keychainIdentity: null,
|
|
444
424
|
p12Path: null,
|
|
445
425
|
p12Password: null,
|
|
446
426
|
chiselVersion: null,
|
|
@@ -499,7 +479,7 @@ async function runTokenSetupJson(flags) {
|
|
|
499
479
|
},
|
|
500
480
|
{
|
|
501
481
|
key: 'import_cert',
|
|
502
|
-
title:
|
|
482
|
+
title: 'Storing certificate',
|
|
503
483
|
fn: async () => {
|
|
504
484
|
const result = await storeEnrolledCert(
|
|
505
485
|
ctx._keyData.keyPath,
|
|
@@ -508,12 +488,8 @@ async function runTokenSetupJson(flags) {
|
|
|
508
488
|
ctx.resolvedLabel,
|
|
509
489
|
{ log: () => {}, warn: () => {}, error: () => {} },
|
|
510
490
|
);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
} else {
|
|
514
|
-
ctx.p12Path = result.p12Path;
|
|
515
|
-
ctx.p12Password = result.p12Password;
|
|
516
|
-
}
|
|
491
|
+
ctx.p12Path = result.p12Path;
|
|
492
|
+
ctx.p12Password = result.p12Password;
|
|
517
493
|
},
|
|
518
494
|
},
|
|
519
495
|
{
|
|
@@ -528,9 +504,7 @@ async function runTokenSetupJson(flags) {
|
|
|
528
504
|
key: 'verify_connectivity',
|
|
529
505
|
title: 'Verifying panel connectivity',
|
|
530
506
|
fn: async () => {
|
|
531
|
-
const authConfig = ctx.
|
|
532
|
-
? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
|
|
533
|
-
: { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
507
|
+
const authConfig = { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
534
508
|
await fetchHealth(authConfig);
|
|
535
509
|
},
|
|
536
510
|
},
|
|
@@ -546,9 +520,7 @@ async function runTokenSetupJson(flags) {
|
|
|
546
520
|
key: 'fetch_config',
|
|
547
521
|
title: 'Fetching tunnel configuration',
|
|
548
522
|
fn: async () => {
|
|
549
|
-
const authConfig = ctx.
|
|
550
|
-
? { panelUrl: ctx.panelUrl, authMethod: 'keychain', keychainIdentity: ctx.keychainIdentity }
|
|
551
|
-
: { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
523
|
+
const authConfig = { panelUrl: ctx.panelUrl, authMethod: 'p12', p12Path: ctx.p12Path, p12Password: ctx.p12Password };
|
|
552
524
|
|
|
553
525
|
const agentConfig = await fetchAgentConfig(authConfig);
|
|
554
526
|
ctx.domain = agentConfig.domain;
|
|
@@ -605,29 +577,23 @@ async function runTokenSetupJson(flags) {
|
|
|
605
577
|
fn: async () => {
|
|
606
578
|
const configData = {
|
|
607
579
|
panelUrl: ctx.panelUrl,
|
|
580
|
+
authMethod: 'p12',
|
|
581
|
+
p12Path: ctx.p12Path,
|
|
582
|
+
p12Password: ctx.p12Password,
|
|
608
583
|
agentLabel: ctx.agentLabel,
|
|
609
584
|
domain: ctx.domain,
|
|
610
585
|
chiselVersion: ctx.chiselVersion,
|
|
611
586
|
setupAt: new Date().toISOString(),
|
|
612
587
|
};
|
|
613
588
|
|
|
614
|
-
if (ctx.keychainIdentity) {
|
|
615
|
-
configData.authMethod = 'keychain';
|
|
616
|
-
configData.keychainIdentity = ctx.keychainIdentity;
|
|
617
|
-
} else {
|
|
618
|
-
configData.authMethod = 'p12';
|
|
619
|
-
configData.p12Path = ctx.p12Path;
|
|
620
|
-
configData.p12Password = ctx.p12Password;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
589
|
await saveAgentConfig(ctx.resolvedLabel, configData);
|
|
624
590
|
|
|
625
591
|
await upsertAgent({
|
|
626
592
|
label: ctx.resolvedLabel,
|
|
627
593
|
panelUrl: ctx.panelUrl,
|
|
628
|
-
authMethod:
|
|
629
|
-
p12Path:
|
|
630
|
-
keychainIdentity:
|
|
594
|
+
authMethod: 'p12',
|
|
595
|
+
p12Path: ctx.p12Path,
|
|
596
|
+
keychainIdentity: null,
|
|
631
597
|
agentLabel: ctx.agentLabel,
|
|
632
598
|
domain: ctx.domain,
|
|
633
599
|
chiselVersion: ctx.chiselVersion,
|
|
@@ -650,12 +616,17 @@ async function runTokenSetupJson(flags) {
|
|
|
650
616
|
process.exit(1);
|
|
651
617
|
}
|
|
652
618
|
|
|
619
|
+
// The p12Password transits via stdout pipe to the parent process (Tauri desktop app),
|
|
620
|
+
// which stores it in the OS credential store. Pipes are not visible in process listings.
|
|
621
|
+
// This is the same trust boundary as the server provisioner's SCP-based P12 transfer.
|
|
653
622
|
emitJson({
|
|
654
623
|
event: 'complete',
|
|
655
624
|
agent: {
|
|
656
625
|
label: ctx.resolvedLabel,
|
|
657
626
|
panelUrl: ctx.panelUrl,
|
|
658
|
-
authMethod:
|
|
627
|
+
authMethod: 'p12',
|
|
628
|
+
p12Path: ctx.p12Path,
|
|
629
|
+
p12Password: ctx.p12Password,
|
|
659
630
|
domain: ctx.domain,
|
|
660
631
|
chiselVersion: ctx.chiselVersion,
|
|
661
632
|
},
|
|
@@ -676,9 +647,7 @@ async function runP12Setup(options = {}) {
|
|
|
676
647
|
|
|
677
648
|
console.log('');
|
|
678
649
|
console.log(chalk.bold(' Portlama Agent Setup'));
|
|
679
|
-
console.log(chalk.dim(
|
|
680
|
-
? ' Connect this Mac to your Portlama server.'
|
|
681
|
-
: ' Connect this machine to your Portlama server.'));
|
|
650
|
+
console.log(chalk.dim(' Connect this machine to your Portlama server.'));
|
|
682
651
|
console.log('');
|
|
683
652
|
console.log(chalk.dim(' The admin must generate an agent certificate from the panel first:'));
|
|
684
653
|
console.log(chalk.dim(' Panel → Certificates → Agent Certificates → Generate'));
|
package/src/lib/cert-store.js
CHANGED
|
@@ -1,74 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Portable certificate storage for token-based enrollment.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Stores enrolled certificates as P12 files on all platforms.
|
|
5
|
+
* The P12 password is returned to the caller for secure storage
|
|
6
|
+
* (e.g., macOS Keychain via security-framework, Linux libsecret).
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import crypto from 'node:crypto';
|
|
8
10
|
import { writeFile, access, constants } from 'node:fs/promises';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
import { execa } from 'execa';
|
|
11
|
-
import {
|
|
13
|
+
import { agentDataDir } from './platform.js';
|
|
12
14
|
import { secureDelete } from './keychain.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Store an enrolled certificate
|
|
17
|
+
* Store an enrolled certificate as a P12 file.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Creates a P12 bundle from key + cert + CA at
|
|
20
|
+
* ~/.portlama/agents/<label>/client.p12 with mode 0600.
|
|
19
21
|
*
|
|
20
22
|
* @param {string} keyPath - Path to the temporary private key PEM
|
|
21
23
|
* @param {string} certPem - PEM-encoded signed certificate
|
|
22
24
|
* @param {string} caCertPem - PEM-encoded CA certificate
|
|
23
25
|
* @param {string} label - Agent label
|
|
24
26
|
* @param {import('pino').Logger | Console} logger
|
|
25
|
-
* @returns {Promise<{
|
|
27
|
+
* @returns {Promise<{ p12Path: string, p12Password: string }>}
|
|
26
28
|
*/
|
|
27
29
|
export async function storeEnrolledCert(keyPath, certPem, caCertPem, label, logger) {
|
|
28
|
-
if (isDarwin()) {
|
|
29
|
-
const { importIdentityToKeychain } = await import('./keychain.js');
|
|
30
|
-
const { identity } = await importIdentityToKeychain(keyPath, certPem, caCertPem, label, logger);
|
|
31
|
-
return { identity };
|
|
32
|
-
}
|
|
33
|
-
return storeP12Linux(keyPath, certPem, caCertPem, label, logger);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if an enrolled certificate exists.
|
|
38
|
-
* @param {string} label - Agent label
|
|
39
|
-
* @returns {Promise<boolean>}
|
|
40
|
-
*/
|
|
41
|
-
export async function enrolledCertExists(label) {
|
|
42
|
-
if (isDarwin()) {
|
|
43
|
-
const { keychainIdentityExists } = await import('./keychain.js');
|
|
44
|
-
return keychainIdentityExists(label);
|
|
45
|
-
}
|
|
46
|
-
return linuxP12Exists(label);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Remove an enrolled certificate.
|
|
51
|
-
* @param {string} label - Agent label
|
|
52
|
-
*/
|
|
53
|
-
export async function removeEnrolledCert(label) {
|
|
54
|
-
if (isDarwin()) {
|
|
55
|
-
const { removeKeychainIdentity } = await import('./keychain.js');
|
|
56
|
-
return removeKeychainIdentity(label);
|
|
57
|
-
}
|
|
58
|
-
return removeLinuxP12(label);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Linux — P12 file storage
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Create a P12 from key + cert + CA and store at ~/.portlama/client.p12.
|
|
67
|
-
*
|
|
68
|
-
* Uses the same `-keypbe PBE-SHA1-3DES` parameters as keychain.js for
|
|
69
|
-
* maximum curl compatibility.
|
|
70
|
-
*/
|
|
71
|
-
async function storeP12Linux(keyPath, certPem, caCertPem, label, logger) {
|
|
72
30
|
const dataDir = agentDataDir(label);
|
|
73
31
|
const p12Path = path.join(dataDir, 'client.p12');
|
|
74
32
|
const suffix = crypto.randomBytes(8).toString('hex');
|
|
@@ -124,7 +82,12 @@ async function storeP12Linux(keyPath, certPem, caCertPem, label, logger) {
|
|
|
124
82
|
}
|
|
125
83
|
}
|
|
126
84
|
|
|
127
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Check if an enrolled certificate exists.
|
|
87
|
+
* @param {string} label - Agent label
|
|
88
|
+
* @returns {Promise<boolean>}
|
|
89
|
+
*/
|
|
90
|
+
export async function enrolledCertExists(label) {
|
|
128
91
|
try {
|
|
129
92
|
const p12Path = path.join(agentDataDir(label), 'client.p12');
|
|
130
93
|
await access(p12Path, constants.F_OK);
|
|
@@ -134,7 +97,11 @@ async function linuxP12Exists(label) {
|
|
|
134
97
|
}
|
|
135
98
|
}
|
|
136
99
|
|
|
137
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Remove an enrolled certificate.
|
|
102
|
+
* @param {string} label - Agent label
|
|
103
|
+
*/
|
|
104
|
+
export async function removeEnrolledCert(label) {
|
|
138
105
|
try {
|
|
139
106
|
const p12Path = path.join(agentDataDir(label), 'client.p12');
|
|
140
107
|
await secureDelete(p12Path);
|
package/src/lib/keychain.js
CHANGED
|
@@ -4,6 +4,23 @@ import path from 'node:path';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { AGENT_DIR } from './platform.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Prompt for the macOS login Keychain password via a native OS dialog.
|
|
9
|
+
* Uses osascript to display a secure input dialog (hidden answer).
|
|
10
|
+
* Throws if the user cancels.
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<string>}
|
|
13
|
+
*/
|
|
14
|
+
async function promptKeychainPassword() {
|
|
15
|
+
const { stdout } = await execa('osascript', [
|
|
16
|
+
'-e',
|
|
17
|
+
'display dialog "Portlama needs your macOS login password to store the agent certificate in your Keychain." default answer "" with hidden answer buttons {"Cancel", "OK"} default button "OK" with title "Portlama — Keychain Access" with icon caution',
|
|
18
|
+
'-e',
|
|
19
|
+
'text returned of result',
|
|
20
|
+
]);
|
|
21
|
+
return stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
/**
|
|
8
25
|
* Overwrite a file with random bytes, then unlink it.
|
|
9
26
|
* Provides defense-in-depth against key recovery from disk.
|
|
@@ -159,23 +176,22 @@ export async function importIdentityToKeychain(keyPath, certPem, caCertPem, labe
|
|
|
159
176
|
}
|
|
160
177
|
|
|
161
178
|
// Set the key partition list so curl can access the identity without prompts.
|
|
162
|
-
//
|
|
163
|
-
|
|
179
|
+
// Requires the login Keychain password — prompt via native macOS dialog.
|
|
180
|
+
const keychainPassword = await promptKeychainPassword();
|
|
164
181
|
try {
|
|
165
182
|
await execa('security', [
|
|
166
183
|
'set-key-partition-list',
|
|
167
184
|
'-S',
|
|
168
185
|
'apple-tool:,apple:', // apple-tool: for /usr/bin/curl, apple: for GUI apps
|
|
169
186
|
'-k',
|
|
170
|
-
|
|
187
|
+
keychainPassword,
|
|
171
188
|
'-l', // match by Label (friendly name from -name), not -D (Description)
|
|
172
189
|
identityName,
|
|
173
190
|
]);
|
|
174
|
-
} catch
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
logger.log?.(`Warning: Could not set key partition list for ${label}`);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(
|
|
193
|
+
'Could not authorize Keychain access. The macOS login password may be incorrect.',
|
|
194
|
+
);
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
return { identity: identityName };
|