@seekrit/cli 0.7.0 → 0.8.0

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import { Command } from "commander";
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { homedir } from "node:os";
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir, tmpdir } from "node:os";
6
6
  import { dirname, join, parse } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { Writable } from "node:stream";
@@ -259,6 +259,138 @@ async function generatePostgresCredential(options = {}) {
259
259
  };
260
260
  }
261
261
  //#endregion
262
+ //#region ../../packages/crypto/src/ssh.ts
263
+ /**
264
+ * Client-side SSH certificate authority for minting *temporary SSH access*
265
+ * without any private key ever reaching seekrit's control plane.
266
+ *
267
+ * The model (Vault-style, tier-1 verifier injection — see the `ZkTier` doc in
268
+ * @seekrit/core leases.ts):
269
+ *
270
+ * 1. the machine that will connect generates an ephemeral Ed25519 keypair
271
+ * locally ({@link generateSshKeyPair}),
272
+ * 2. it sends only the *public* key to the broker,
273
+ * 3. the broker signs a short-lived OpenSSH user *certificate* over that
274
+ * public key ({@link signSshUserCertificate}) using the CA private key,
275
+ * 4. the consumer connects with `ssh -i <key> -o CertificateFile=<cert>`; the
276
+ * host trusts the CA (`TrustedUserCAKeys`) and never needs the key on disk.
277
+ *
278
+ * Zero-knowledge end to end: the user private key exists only on the consumer,
279
+ * and a certificate is a public artifact (it authorizes but cannot authenticate
280
+ * without the matching private key). The only secret seekrit stores is the CA
281
+ * private key — wrapped to the broker DO's public key, decrypted transiently in
282
+ * the DO to sign (the same in-DO trade the Postgres admin credential makes).
283
+ *
284
+ * Everything here is WebCrypto Ed25519 + hand-rolled SSH wire encoding, so it
285
+ * runs unchanged in the browser, the CLI, and Workers. No Node-specific crypto.
286
+ */
287
+ const ED25519 = { name: "Ed25519" };
288
+ /**
289
+ * Serializer for the SSH binary wire types. `string` is a uint32 length prefix
290
+ * followed by that many bytes (a length-delimited byte blob, not text); ints
291
+ * are big-endian. This is the encoding used by public keys, certificates, and
292
+ * the OpenSSH private key container alike.
293
+ */
294
+ var SshWriter = class {
295
+ chunks = [];
296
+ len = 0;
297
+ bytes(b) {
298
+ this.chunks.push(b);
299
+ this.len += b.length;
300
+ return this;
301
+ }
302
+ byte(n) {
303
+ return this.bytes(new Uint8Array([n & 255]));
304
+ }
305
+ uint32(n) {
306
+ const b = /* @__PURE__ */ new Uint8Array(4);
307
+ new DataView(b.buffer).setUint32(0, n >>> 0, false);
308
+ return this.bytes(b);
309
+ }
310
+ uint64(n) {
311
+ const b = /* @__PURE__ */ new Uint8Array(8);
312
+ new DataView(b.buffer).setBigUint64(0, BigInt(n), false);
313
+ return this.bytes(b);
314
+ }
315
+ string(s) {
316
+ const b = typeof s === "string" ? utf8Encode(s) : s;
317
+ return this.uint32(b.length).bytes(b);
318
+ }
319
+ build() {
320
+ const out = new Uint8Array(this.len);
321
+ let o = 0;
322
+ for (const c of this.chunks) {
323
+ out.set(c, o);
324
+ o += c.length;
325
+ }
326
+ return out;
327
+ }
328
+ };
329
+ function concat(...arrays) {
330
+ const total = arrays.reduce((n, a) => n + a.length, 0);
331
+ const out = new Uint8Array(total);
332
+ let o = 0;
333
+ for (const a of arrays) {
334
+ out.set(a, o);
335
+ o += a.length;
336
+ }
337
+ return out;
338
+ }
339
+ /** The `ssh-ed25519` public-key blob: string "ssh-ed25519" ‖ string <32 bytes>. */
340
+ function ed25519PublicKeyBlob(pub) {
341
+ return new SshWriter().string("ssh-ed25519").string(pub).build();
342
+ }
343
+ function encodeSshPublicKey(pub, comment) {
344
+ const b64 = toBase64(ed25519PublicKeyBlob(pub));
345
+ return comment ? `ssh-ed25519 ${b64} ${comment}` : `ssh-ed25519 ${b64}`;
346
+ }
347
+ /**
348
+ * Generate an ephemeral client keypair. The private key is serialized in the
349
+ * `openssh-key-v1` format so `ssh -i` accepts it directly; the public key is
350
+ * what gets certified. Nothing here is ever sent to the control plane except
351
+ * the public key.
352
+ */
353
+ async function generateSshKeyPair(comment = "seekrit") {
354
+ const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
355
+ const pub = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
356
+ const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey));
357
+ const seed = pkcs8.subarray(pkcs8.length - 32);
358
+ return {
359
+ publicKeyOpenssh: encodeSshPublicKey(pub, comment),
360
+ privateKeyOpenssh: encodeOpensshPrivateKey(seed, pub, comment)
361
+ };
362
+ }
363
+ /**
364
+ * Generate a certificate-authority keypair. The private half is exported as a
365
+ * JWK (so the broker can re-import it to sign); the public half is printed for
366
+ * admins to install on their hosts.
367
+ */
368
+ async function generateSshCaKeyPair(comment = "seekrit-ca") {
369
+ const pair = await crypto.subtle.generateKey(ED25519, true, ["sign", "verify"]);
370
+ const pub = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
371
+ const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
372
+ return {
373
+ privateKeyJwk: JSON.stringify(jwk),
374
+ publicKeyOpenssh: encodeSshPublicKey(pub, comment)
375
+ };
376
+ }
377
+ /**
378
+ * Serialize an Ed25519 keypair as an unencrypted `openssh-key-v1` private key.
379
+ * Format (PROTOCOL.key): magic ‖ ciphername "none" ‖ kdfname "none" ‖ empty
380
+ * kdfoptions ‖ nkeys=1 ‖ public-key blob ‖ private section (wrapped as a
381
+ * string). The private section is two equal check-ints, then the key, then the
382
+ * comment, padded with 1,2,3,… to the "none" block size (8).
383
+ */
384
+ function encodeOpensshPrivateKey(seed, pub, comment) {
385
+ const check = new DataView(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(4)).buffer).getUint32(0, false);
386
+ const privSection = new SshWriter().uint32(check).uint32(check).string("ssh-ed25519").string(pub).string(concat(seed, pub)).string(comment).build();
387
+ const padLen = (8 - privSection.length % 8) % 8;
388
+ const pad = new Uint8Array(padLen);
389
+ for (let i = 0; i < padLen; i++) pad[i] = i + 1;
390
+ const b64 = toBase64(new SshWriter().bytes(utf8Encode("openssh-key-v1")).byte(0).string("none").string("none").string(/* @__PURE__ */ new Uint8Array(0)).uint32(1).string(ed25519PublicKeyBlob(pub)).string(concat(privSection, pad)).build());
391
+ return `-----BEGIN OPENSSH PRIVATE KEY-----\n${b64.match(/.{1,70}/g)?.join("\n") ?? b64}\n-----END OPENSSH PRIVATE KEY-----\n`;
392
+ }
393
+ //#endregion
262
394
  //#region ../../packages/crypto/src/token.ts
263
395
  /**
264
396
  * Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
@@ -385,7 +517,7 @@ async function unwrapDek(wrapped, privateKey) {
385
517
  }
386
518
  //#endregion
387
519
  //#region package.json
388
- var version = "0.7.0";
520
+ var version = "0.8.0";
389
521
  const PROJECT_FILE = "seekrit.json";
390
522
  function globalConfigPath() {
391
523
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
@@ -592,7 +724,6 @@ var SeekritClient = class {
592
724
  listLeases(orgId) {
593
725
  return this.request("GET", `/v1/orgs/${orgId}/leases`);
594
726
  }
595
- /** Mint a temporary credential. Send only the client-computed verifier. */
596
727
  mintLease(orgId, input) {
597
728
  return this.request("POST", `/v1/orgs/${orgId}/leases`, input);
598
729
  }
@@ -733,7 +864,7 @@ function formatSecrets(values, format) {
733
864
  case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
734
865
  }
735
866
  }
736
- z.enum(["postgres"]);
867
+ z.enum(["postgres", "ssh"]);
737
868
  const executorModeSchema = z.enum(["in_do", "remote"]);
738
869
  /**
739
870
  * A Postgres role name we are willing to create. Deliberately strict — this
@@ -747,6 +878,16 @@ const postgresRoleNameSchema = z.string().regex(/^[a-z_][a-z0-9_]{2,62}$/, "must
747
878
  * base64 + the fixed structural characters, none of which is a single quote).
748
879
  */
749
880
  const scramVerifierSchema = z.string().regex(/^SCRAM-SHA-256\$\d{3,}:[A-Za-z0-9+/=]+\$[A-Za-z0-9+/=]+:[A-Za-z0-9+/=]+$/, "must be a SCRAM-SHA-256 verifier");
881
+ /**
882
+ * An SSH login principal (a Unix-style username the certificate authorizes).
883
+ * Bounded and restricted to a safe charset — principals are SSH-wire-encoded,
884
+ * not shell-interpolated, so this is sanity/DoS hardening, not an injection gate.
885
+ */
886
+ const sshPrincipalSchema = z.string().regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1–64 chars of letters, digits, dot, dash, underscore");
887
+ /** An `ssh-ed25519 <base64> [comment]` public key line (deep-validated on sign). */
888
+ const sshPublicKeySchema = z.string().max(2048).regex(/^ssh-ed25519 [A-Za-z0-9+/=]+( .*)?$/, "must be an ssh-ed25519 public key");
889
+ /** An SSH certificate extension name, e.g. `permit-pty`. */
890
+ const sshExtensionSchema = z.string().regex(/^[a-z0-9-]{1,64}$/);
750
891
  const postgresAccessLevelSchema = z.enum([
751
892
  "readonly",
752
893
  "readwrite",
@@ -766,7 +907,7 @@ const connectionSchema = z.object({
766
907
  const statementSchema = z.string().min(1).max(4e3);
767
908
  /** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
768
909
  const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
769
- const leaseTargetConfigSchema = z.object({
910
+ const postgresTargetConfigSchema = z.object({
770
911
  provider: z.literal("postgres"),
771
912
  executor: executorModeSchema,
772
913
  accessLevel: postgresAccessLevelSchema.optional(),
@@ -776,6 +917,19 @@ const leaseTargetConfigSchema = z.object({
776
917
  createStatements: z.array(statementSchema).max(16).optional(),
777
918
  revokeStatements: z.array(statementSchema).max(16).optional()
778
919
  });
920
+ const sshTargetConfigSchema = z.object({
921
+ provider: z.literal("ssh"),
922
+ executor: z.literal("in_do"),
923
+ caPublicKey: sshPublicKeySchema,
924
+ allowedPrincipals: z.array(sshPrincipalSchema).max(64).optional(),
925
+ extensions: z.array(sshExtensionSchema).max(16).optional(),
926
+ maxTtlSeconds: z.number().int().min(60).max(3600 * 24 * 7).optional(),
927
+ connection: z.object({
928
+ host: z.string().min(1).optional(),
929
+ user: sshPrincipalSchema.optional()
930
+ }).optional()
931
+ });
932
+ const leaseTargetConfigSchema = z.discriminatedUnion("provider", [postgresTargetConfigSchema, sshTargetConfigSchema]);
779
933
  z.object({
780
934
  name: z.string().trim().min(1).max(128),
781
935
  config: leaseTargetConfigSchema,
@@ -786,12 +940,33 @@ z.object({
786
940
  */
787
941
  wrappedAdminSecret: z.string().min(1)
788
942
  });
789
- z.object({
943
+ /** Requested lease lifetime, shared by all providers. */
944
+ const ttlSecondsSchema = z.number().int().min(60).max(3600 * 24 * 7);
945
+ /**
946
+ * Client → API: mint a Postgres lease. The client generates the password and
947
+ * its SCRAM verifier locally and sends only the verifier — the plaintext
948
+ * password never leaves the requesting machine.
949
+ */
950
+ const mintPostgresLeaseSchema = z.object({
951
+ provider: z.literal("postgres"),
790
952
  targetId: z.string().min(1),
791
953
  roleName: postgresRoleNameSchema,
792
954
  verifier: scramVerifierSchema,
793
- ttlSeconds: z.number().int().min(60).max(3600 * 24 * 7)
955
+ ttlSeconds: ttlSecondsSchema
794
956
  });
957
+ /**
958
+ * Client → API: mint an SSH lease. The client generates an ephemeral keypair
959
+ * locally and sends only the public key; the signed certificate comes back in
960
+ * the response. The private key never leaves the requesting machine.
961
+ */
962
+ const mintSshLeaseSchema = z.object({
963
+ provider: z.literal("ssh"),
964
+ targetId: z.string().min(1),
965
+ publicKey: sshPublicKeySchema,
966
+ principals: z.array(sshPrincipalSchema).min(1).max(32),
967
+ ttlSeconds: ttlSecondsSchema
968
+ });
969
+ z.discriminatedUnion("provider", [mintPostgresLeaseSchema, mintSshLeaseSchema]);
795
970
  //#endregion
796
971
  //#region ../../packages/core/src/providers/postgres.ts
797
972
  /**
@@ -825,6 +1000,27 @@ function postgresGroupBootstrapSql(config) {
825
1000
  return lines.join("\n");
826
1001
  }
827
1002
  //#endregion
1003
+ //#region ../../packages/core/src/providers/ssh.ts
1004
+ /**
1005
+ * The one-time host setup an admin runs so a target's certificates are accepted.
1006
+ * Analogue of `postgresGroupBootstrapSql` — displayed for the admin to run, not
1007
+ * executed by seekrit. Embeds the CA public key so it's copy-paste runnable.
1008
+ */
1009
+ function sshHostSetupInstructions(config) {
1010
+ const principals = config.allowedPrincipals?.length ? config.allowedPrincipals : ["<login-user>"];
1011
+ return [
1012
+ "# Run once on each target host so it trusts seekrit-issued certificates.",
1013
+ "# 1. Install the CA public key and trust it for user authentication:",
1014
+ `echo '${config.caPublicKey}' | sudo tee /etc/ssh/seekrit_ca.pub`,
1015
+ "sudo sh -c 'echo \"TrustedUserCAKeys /etc/ssh/seekrit_ca.pub\" >> /etc/ssh/sshd_config'",
1016
+ "# 2. (optional) Restrict which cert principals may log in as which users via",
1017
+ "# AuthorizedPrincipalsFile, e.g. /etc/ssh/auth_principals/<user> listing:",
1018
+ ...principals.map((p) => `# ${p}`),
1019
+ "# 3. Reload sshd:",
1020
+ "sudo systemctl reload sshd"
1021
+ ].join("\n");
1022
+ }
1023
+ //#endregion
828
1024
  //#region ../../packages/core/src/schemas.ts
829
1025
  /** URL-safe identifier segment: `my-app`, `production`, … */
830
1026
  const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
@@ -1003,7 +1199,7 @@ async function resolveGroup(ctx, opts) {
1003
1199
  * ciphertext.
1004
1200
  */
1005
1201
  /** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
1006
- function parseTtlSeconds(input) {
1202
+ function parseTtlSeconds$1(input) {
1007
1203
  const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
1008
1204
  if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
1009
1205
  return Number(m[1]) * ({
@@ -1024,7 +1220,7 @@ function generateRoleName(prefix = "tmp") {
1024
1220
  function registerPgCommands(program) {
1025
1221
  const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
1026
1222
  const target = pg.command("target").description("manage provisioning targets");
1027
- target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--admin-url <url>", "admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect, []).action(async (options) => {
1223
+ target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--admin-url <url>", "admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$1, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$1, []).action(async (options) => {
1028
1224
  const ctx = buildContext();
1029
1225
  const org = await resolveOrg(ctx, options.org);
1030
1226
  const executor = options.executor === "remote" ? "remote" : "in_do";
@@ -1072,6 +1268,7 @@ function registerPgCommands(program) {
1072
1268
  const { targets } = await ctx.client.listLeaseTargets(org.id);
1073
1269
  for (const t of targets) {
1074
1270
  const cfg = t.config;
1271
+ if (cfg.provider !== "postgres") continue;
1075
1272
  console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
1076
1273
  }
1077
1274
  });
@@ -1081,7 +1278,9 @@ function registerPgCommands(program) {
1081
1278
  const { targets } = await ctx.client.listLeaseTargets(org.id);
1082
1279
  const t = targets.find((x) => x.id === targetId || x.name === targetId);
1083
1280
  if (!t) fail(`no target "${targetId}" in ${org.slug}`);
1084
- const bootstrap = postgresGroupBootstrapSql(t.config);
1281
+ const cfg = t.config;
1282
+ if (cfg.provider !== "postgres") fail("not a postgres target (see `seekrit ssh`)");
1283
+ const bootstrap = postgresGroupBootstrapSql(cfg);
1085
1284
  if (!bootstrap) fail("this is a custom target — it has no generated setup SQL");
1086
1285
  console.log(bootstrap);
1087
1286
  });
@@ -1097,10 +1296,12 @@ function registerPgCommands(program) {
1097
1296
  const { targets } = await ctx.client.listLeaseTargets(org.id);
1098
1297
  const target = targets.find((t) => t.id === targetRef || t.name === targetRef);
1099
1298
  if (!target) fail(`no target "${targetRef}" in ${org.slug}`);
1299
+ if (target.config.provider !== "postgres") fail(`"${target.name}" is not a postgres target (see \`seekrit ssh\`)`);
1100
1300
  const roleName = options.role ?? generateRoleName();
1101
- const ttlSeconds = parseTtlSeconds(options.ttl);
1301
+ const ttlSeconds = parseTtlSeconds$1(options.ttl);
1102
1302
  const { password, verifier } = await generatePostgresCredential();
1103
1303
  const { connection } = await ctx.client.mintLease(org.id, {
1304
+ provider: "postgres",
1104
1305
  targetId: target.id,
1105
1306
  roleName,
1106
1307
  verifier,
@@ -1129,7 +1330,7 @@ function registerPgCommands(program) {
1129
1330
  });
1130
1331
  }
1131
1332
  /** Collect a repeatable option into an array. */
1132
- function collect(value, acc) {
1333
+ function collect$1(value, acc) {
1133
1334
  acc.push(value);
1134
1335
  return acc;
1135
1336
  }
@@ -1234,6 +1435,147 @@ function printExplain(provenance) {
1234
1435
  for (const name of names) process.stderr.write(`${name.padEnd(width)} ${provenance[name]}\n`);
1235
1436
  }
1236
1437
  //#endregion
1438
+ //#region src/ssh.ts
1439
+ /**
1440
+ * `seekrit ssh` — temporary SSH access via short-lived certificates (Vault-style
1441
+ * dynamic secrets, the SSH sibling of `seekrit pg`).
1442
+ *
1443
+ * Zero-knowledge: minting generates an ephemeral keypair on THIS machine and
1444
+ * sends only the public key; the broker signs a certificate and returns it (a
1445
+ * public artifact). The private key never leaves this machine. Registering a
1446
+ * target generates a CA keypair locally and wraps the CA *private* key to the
1447
+ * broker's public key, so the control plane only ever stores ciphertext; the CA
1448
+ * *public* key is printed for you to install on your hosts.
1449
+ */
1450
+ /** Parse a duration like `30m`, `1h`, `7d`, or a bare seconds count. */
1451
+ function parseTtlSeconds(input) {
1452
+ const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
1453
+ if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 7d)`);
1454
+ return Number(m[1]) * ({
1455
+ s: 1,
1456
+ m: 60,
1457
+ h: 3600,
1458
+ d: 86400
1459
+ }[m[2] || "s"] ?? 1);
1460
+ }
1461
+ function registerSshCommands(program) {
1462
+ const ssh = program.command("ssh").description("temporary SSH access (short-lived certificates, zero-knowledge)");
1463
+ const target = ssh.command("target").description("manage SSH CA targets");
1464
+ target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
1465
+ const ctx = buildContext();
1466
+ const org = await resolveOrg(ctx, options.org);
1467
+ const ca = await generateSshCaKeyPair(`seekrit-ca:${options.name}`);
1468
+ const config = {
1469
+ provider: "ssh",
1470
+ executor: "in_do",
1471
+ caPublicKey: ca.publicKeyOpenssh,
1472
+ ...options.principal.length ? { allowedPrincipals: options.principal } : {},
1473
+ ...options.extension.length ? { extensions: options.extension } : {},
1474
+ ...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds(options.maxTtl) } : {},
1475
+ ...options.host || options.user ? { connection: {
1476
+ ...options.host ? { host: options.host } : {},
1477
+ ...options.user ? { user: options.user } : {}
1478
+ } } : {}
1479
+ };
1480
+ const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
1481
+ const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(ca.privateKeyJwk), publicKeyJwk);
1482
+ const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
1483
+ name: options.name,
1484
+ config,
1485
+ wrappedAdminSecret
1486
+ });
1487
+ console.error(`registered SSH target ${created.name} (${created.id})`);
1488
+ console.error("\nInstall the CA on your hosts, then issue certs with `seekrit ssh lease`:\n");
1489
+ console.log(sshHostSetupInstructions(config));
1490
+ });
1491
+ target.command("list").description("list SSH CA targets").option("--org <slug>").action(async (options) => {
1492
+ const ctx = buildContext();
1493
+ const org = await resolveOrg(ctx, options.org);
1494
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
1495
+ for (const t of targets) {
1496
+ const cfg = t.config;
1497
+ if (cfg.provider !== "ssh") continue;
1498
+ const where = cfg.connection?.host ?? "-";
1499
+ const principals = cfg.allowedPrincipals?.join(",") ?? "any";
1500
+ console.log(`${t.id}\t${t.name}\t${where}\tprincipals=${principals}`);
1501
+ }
1502
+ });
1503
+ target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>").action(async (targetId, options) => {
1504
+ const ctx = buildContext();
1505
+ const org = await resolveOrg(ctx, options.org);
1506
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
1507
+ const t = targets.find((x) => x.id === targetId || x.name === targetId);
1508
+ if (!t) fail(`no target "${targetId}" in ${org.slug}`);
1509
+ const cfg = t.config;
1510
+ if (cfg.provider !== "ssh") fail("not an ssh target (see `seekrit pg`)");
1511
+ console.log(sshHostSetupInstructions(cfg));
1512
+ });
1513
+ target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>").action(async (targetId, options) => {
1514
+ const ctx = buildContext();
1515
+ const org = await resolveOrg(ctx, options.org);
1516
+ await ctx.client.deleteLeaseTarget(org.id, targetId);
1517
+ console.error(`deleted ${targetId}`);
1518
+ });
1519
+ ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
1520
+ const ctx = buildContext();
1521
+ const org = await resolveOrg(ctx, options.org);
1522
+ const { targets } = await ctx.client.listLeaseTargets(org.id);
1523
+ const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
1524
+ if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
1525
+ const cfg = t.config;
1526
+ if (cfg.provider !== "ssh") fail(`"${t.name}" is not an ssh target (see \`seekrit pg\`)`);
1527
+ const principals = options.principal.length ? options.principal : cfg.connection?.user ? [cfg.connection.user] : cfg.allowedPrincipals ?? [];
1528
+ if (principals.length === 0) fail("specify at least one --principal");
1529
+ const ttlSeconds = parseTtlSeconds(options.ttl);
1530
+ const keypair = await generateSshKeyPair(`seekrit:${t.name}`);
1531
+ const { ssh: cert } = await ctx.client.mintLease(org.id, {
1532
+ provider: "ssh",
1533
+ targetId: t.id,
1534
+ publicKey: keypair.publicKeyOpenssh,
1535
+ principals,
1536
+ ttlSeconds
1537
+ });
1538
+ const dir = options.out ?? mkdtempSync(join(tmpdir(), "seekrit-ssh-"));
1539
+ if (options.out) mkdirSync(dir, { recursive: true });
1540
+ const keyPath = join(dir, "id_ed25519");
1541
+ const certPath = join(dir, "id_ed25519-cert.pub");
1542
+ writeFileSync(keyPath, keypair.privateKeyOpenssh, { mode: 384 });
1543
+ writeFileSync(certPath, `${cert.certificate}\n`, { mode: 420 });
1544
+ const loginUser = principals[0];
1545
+ const command = `ssh -i ${keyPath} -o CertificateFile=${certPath}${cert.host ? ` ${loginUser}@${cert.host}` : ""}`;
1546
+ console.error(`issued cert for ${principals.join(",")} — expires ${cert.expiresAt}`);
1547
+ if (options.json) console.log(JSON.stringify({
1548
+ keyPath,
1549
+ certPath,
1550
+ certificate: cert.certificate,
1551
+ principals,
1552
+ command,
1553
+ expiresAt: cert.expiresAt
1554
+ }, null, 2));
1555
+ else console.log(command);
1556
+ });
1557
+ ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
1558
+ const ctx = buildContext();
1559
+ const org = await resolveOrg(ctx, options.org);
1560
+ const { leases } = await ctx.client.listLeases(org.id);
1561
+ for (const l of leases) {
1562
+ if (l.provider !== "ssh") continue;
1563
+ console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
1564
+ }
1565
+ });
1566
+ ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>").action(async (leaseId, options) => {
1567
+ const ctx = buildContext();
1568
+ const org = await resolveOrg(ctx, options.org);
1569
+ await ctx.client.revokeLease(org.id, leaseId);
1570
+ console.error(`revoked ${leaseId} (issued certs remain valid until they expire)`);
1571
+ });
1572
+ }
1573
+ /** Collect a repeatable option into an array. */
1574
+ function collect(value, acc) {
1575
+ acc.push(value);
1576
+ return acc;
1577
+ }
1578
+ //#endregion
1237
1579
  //#region src/index.ts
1238
1580
  /** Collect repeated `--with group=env` flags into a map. */
1239
1581
  function collectKv(value, acc = {}) {
@@ -1591,8 +1933,9 @@ token.command("revoke <tokenId>").description("revoke a service token").option("
1591
1933
  console.error(`${tokenId} revoked`);
1592
1934
  });
1593
1935
  registerPgCommands(program);
1936
+ registerSshCommands(program);
1594
1937
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
1595
- const { runMcpServer } = await import("./mcp-Dw3hVhbC.js");
1938
+ const { runMcpServer } = await import("./mcp-DNUBSbcd.js");
1596
1939
  await runMcpServer();
1597
1940
  });
1598
1941
  program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
@@ -560,6 +560,7 @@ async function runMcpServer() {
560
560
  const roleName = role ?? `tmp_${randomLower(12)}`;
561
561
  const { password, verifier } = await generatePostgresCredential();
562
562
  const { lease, connection } = await ctx.client.mintLease(orgRef.id, {
563
+ provider: "postgres",
563
564
  targetId: t.id,
564
565
  roleName,
565
566
  verifier,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {