@seekrit/cli 0.15.0 → 0.16.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 +269 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,7 +13,8 @@ z.enum([
|
|
|
13
13
|
"ssh",
|
|
14
14
|
"redis",
|
|
15
15
|
"aws",
|
|
16
|
-
"gcp"
|
|
16
|
+
"gcp",
|
|
17
|
+
"mongodb"
|
|
17
18
|
]);
|
|
18
19
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
19
20
|
/**
|
|
@@ -113,6 +114,22 @@ const p256PublicKeyJwkSchema = z.string().max(2048).refine((s) => {
|
|
|
113
114
|
return false;
|
|
114
115
|
}
|
|
115
116
|
}, "must be a JWK-serialized P-256 public key");
|
|
117
|
+
z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1–64 chars of [A-Za-z0-9_-]");
|
|
118
|
+
/**
|
|
119
|
+
* A MongoDB database name — the db a preset role is granted on, or the
|
|
120
|
+
* authentication database a leased user is created in. MongoDB forbids
|
|
121
|
+
* `/\. "$*<>:|?` and the empty string in db names; we keep to a safe subset.
|
|
122
|
+
*/
|
|
123
|
+
const mongoDatabaseNameSchema = z.string().regex(/^[A-Za-z0-9_-]{1,63}$/, "must be 1–63 chars of [A-Za-z0-9_-]");
|
|
124
|
+
/**
|
|
125
|
+
* A single MongoDB role grant `{ role, db }` for a `custom` target — e.g.
|
|
126
|
+
* `{ role: "readWrite", db: "app" }` or a user-defined role. Admin-supplied
|
|
127
|
+
* trusted input, set once at registration; still bounded structurally.
|
|
128
|
+
*/
|
|
129
|
+
const mongoRoleSchema = z.object({
|
|
130
|
+
role: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be a role name"),
|
|
131
|
+
db: mongoDatabaseNameSchema
|
|
132
|
+
});
|
|
116
133
|
const postgresAccessLevelSchema = z.enum([
|
|
117
134
|
"readonly",
|
|
118
135
|
"readwrite",
|
|
@@ -133,6 +150,11 @@ const redisAccessLevelSchema = z.enum([
|
|
|
133
150
|
"readwrite",
|
|
134
151
|
"custom"
|
|
135
152
|
]);
|
|
153
|
+
const mongoAccessLevelSchema = z.enum([
|
|
154
|
+
"readonly",
|
|
155
|
+
"readwrite",
|
|
156
|
+
"custom"
|
|
157
|
+
]);
|
|
136
158
|
const connectionSchema = z.object({
|
|
137
159
|
host: z.string().min(1),
|
|
138
160
|
port: z.number().int().min(1).max(65535),
|
|
@@ -210,13 +232,24 @@ const gcpTargetConfigSchema = z.object({
|
|
|
210
232
|
delegates: z.array(gcpServiceAccountEmailSchema).max(8).optional(),
|
|
211
233
|
maxTtlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
|
|
212
234
|
});
|
|
235
|
+
const mongoTargetConfigSchema = z.object({
|
|
236
|
+
provider: z.literal("mongodb"),
|
|
237
|
+
executor: z.literal("in_do"),
|
|
238
|
+
accessLevel: mongoAccessLevelSchema.optional(),
|
|
239
|
+
connection: connectionSchema,
|
|
240
|
+
authSource: mongoDatabaseNameSchema.optional(),
|
|
241
|
+
roles: z.array(mongoRoleSchema).min(1).max(32).optional(),
|
|
242
|
+
tls: z.boolean().optional(),
|
|
243
|
+
maxTtlSeconds: z.number().int().min(60).max(3600 * 24 * 7).optional()
|
|
244
|
+
});
|
|
213
245
|
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
214
246
|
postgresTargetConfigSchema,
|
|
215
247
|
mysqlTargetConfigSchema,
|
|
216
248
|
redisTargetConfigSchema,
|
|
217
249
|
sshTargetConfigSchema,
|
|
218
250
|
awsTargetConfigSchema,
|
|
219
|
-
gcpTargetConfigSchema
|
|
251
|
+
gcpTargetConfigSchema,
|
|
252
|
+
mongoTargetConfigSchema
|
|
220
253
|
]);
|
|
221
254
|
z.object({
|
|
222
255
|
name: z.string().trim().min(1).max(128),
|
|
@@ -309,13 +342,27 @@ const mintGcpLeaseSchema = z.object({
|
|
|
309
342
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
310
343
|
ttlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
|
|
311
344
|
});
|
|
345
|
+
/**
|
|
346
|
+
* Client → API: mint a MongoDB lease. Like AWS (tier 2), the client generates
|
|
347
|
+
* an ephemeral P-256 keypair locally and sends only the public key; the broker
|
|
348
|
+
* generates the password, runs `createUser`, and returns the credential wrapped
|
|
349
|
+
* to that key. The private key never leaves the requesting machine, so the
|
|
350
|
+
* plaintext credential is only decryptable there.
|
|
351
|
+
*/
|
|
352
|
+
const mintMongoLeaseSchema = z.object({
|
|
353
|
+
provider: z.literal("mongodb"),
|
|
354
|
+
targetId: z.string().min(1),
|
|
355
|
+
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
356
|
+
ttlSeconds: ttlSecondsSchema
|
|
357
|
+
});
|
|
312
358
|
z.discriminatedUnion("provider", [
|
|
313
359
|
mintPostgresLeaseSchema,
|
|
314
360
|
mintMysqlLeaseSchema,
|
|
315
361
|
mintRedisLeaseSchema,
|
|
316
362
|
mintSshLeaseSchema,
|
|
317
363
|
mintAwsLeaseSchema,
|
|
318
|
-
mintGcpLeaseSchema
|
|
364
|
+
mintGcpLeaseSchema,
|
|
365
|
+
mintMongoLeaseSchema
|
|
319
366
|
]);
|
|
320
367
|
//#endregion
|
|
321
368
|
//#region ../../packages/core/src/providers/aws.ts
|
|
@@ -369,6 +416,32 @@ function gcpSetupInstructions(config) {
|
|
|
369
416
|
].join("\n");
|
|
370
417
|
}
|
|
371
418
|
//#endregion
|
|
419
|
+
//#region ../../packages/core/src/providers/mongodb.ts
|
|
420
|
+
/**
|
|
421
|
+
* The one-time setup an admin performs so seekrit can provision users: create a
|
|
422
|
+
* dedicated provisioning user with `userAdmin` on the target database (or
|
|
423
|
+
* `userAdminAnyDatabase` if leases span databases). Printed for the admin to run
|
|
424
|
+
* in `mongosh`, never executed by seekrit — the analogue of
|
|
425
|
+
* `awsTrustPolicyInstructions` / `sshHostSetupInstructions`.
|
|
426
|
+
*/
|
|
427
|
+
function mongoAdminSetupInstructions(config) {
|
|
428
|
+
const authSource = config.authSource ?? "admin";
|
|
429
|
+
const db = config.connection.database;
|
|
430
|
+
return [
|
|
431
|
+
`// Run once in mongosh against the "${authSource}" database. Creates the`,
|
|
432
|
+
"// provisioning user whose connection string you register as the admin secret.",
|
|
433
|
+
`use ${authSource}`,
|
|
434
|
+
"db.createUser({",
|
|
435
|
+
" user: \"seekrit_provisioner\",",
|
|
436
|
+
" pwd: passwordPrompt(),",
|
|
437
|
+
` roles: [{ role: "userAdmin", db: "${db}" }],`,
|
|
438
|
+
"})",
|
|
439
|
+
"",
|
|
440
|
+
`// Register it: seekrit mongodb target add --name ${db} \\`,
|
|
441
|
+
`// --uri "mongodb://seekrit_provisioner:<pwd>@${config.connection.host}:${config.connection.port}/?authSource=${authSource}"`
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
372
445
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
373
446
|
/**
|
|
374
447
|
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
@@ -445,6 +518,8 @@ const NOTIFICATION_TYPES = [
|
|
|
445
518
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
446
519
|
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
447
520
|
const nameSchema = z.string().trim().min(1).max(128);
|
|
521
|
+
/** An email address, normalized to trimmed lowercase before validation. */
|
|
522
|
+
const emailSchema = z.string().trim().toLowerCase().pipe(z.email().max(320));
|
|
448
523
|
/** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
|
|
449
524
|
const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
|
|
450
525
|
z.enum([
|
|
@@ -452,6 +527,8 @@ z.enum([
|
|
|
452
527
|
"admin",
|
|
453
528
|
"member"
|
|
454
529
|
]);
|
|
530
|
+
/** Role a person may be invited at — never `owner` (ownership isn't invitable). */
|
|
531
|
+
const inviteRoleSchema = z.enum(["admin", "member"]);
|
|
455
532
|
const principalTypeSchema = z.enum(["user", "service_token"]);
|
|
456
533
|
/** Org-level capability a service token can hold (never `owner`). */
|
|
457
534
|
const serviceTokenRoleSchema = z.enum(["admin", "member"]);
|
|
@@ -463,6 +540,10 @@ z.object({
|
|
|
463
540
|
name: nameSchema,
|
|
464
541
|
slug: slugSchema
|
|
465
542
|
});
|
|
543
|
+
z.object({
|
|
544
|
+
email: emailSchema,
|
|
545
|
+
role: inviteRoleSchema.default("member")
|
|
546
|
+
});
|
|
466
547
|
z.object({
|
|
467
548
|
name: nameSchema,
|
|
468
549
|
slug: slugSchema
|
|
@@ -784,6 +865,41 @@ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
|
|
|
784
865
|
};
|
|
785
866
|
}
|
|
786
867
|
//#endregion
|
|
868
|
+
//#region ../../packages/crypto/src/mongodb.ts
|
|
869
|
+
/** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
|
|
870
|
+
async function generateMongoRecipientKeyPair() {
|
|
871
|
+
return generateKeyPair();
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Unwrap the `wd1.` blob returned by a MongoDB lease mint using the ephemeral
|
|
875
|
+
* private key generated for that lease, yielding the plaintext credential.
|
|
876
|
+
*/
|
|
877
|
+
async function unwrapMongoCredential(wrapped, privateKeyJwk) {
|
|
878
|
+
const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
|
|
879
|
+
let parsed;
|
|
880
|
+
try {
|
|
881
|
+
parsed = JSON.parse(utf8Decode(bytes));
|
|
882
|
+
} catch {
|
|
883
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential was not valid JSON");
|
|
884
|
+
}
|
|
885
|
+
const c = parsed;
|
|
886
|
+
if (typeof c.username !== "string" || typeof c.password !== "string" || typeof c.database !== "string" || typeof c.authSource !== "string" || typeof c.host !== "string" || typeof c.port !== "number" || typeof c.expiration !== "string") throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential is missing fields");
|
|
887
|
+
return {
|
|
888
|
+
username: c.username,
|
|
889
|
+
password: c.password,
|
|
890
|
+
database: c.database,
|
|
891
|
+
authSource: c.authSource,
|
|
892
|
+
host: c.host,
|
|
893
|
+
port: c.port,
|
|
894
|
+
expiration: c.expiration,
|
|
895
|
+
uri: mongoConnectionUri(c)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/** Build a `mongodb://user:pass@host:port/db?authSource=…` URI from a credential. */
|
|
899
|
+
function mongoConnectionUri(cred) {
|
|
900
|
+
return `mongodb://${encodeURIComponent(cred.username)}:${encodeURIComponent(cred.password)}@${cred.host}:${cred.port}/${cred.database}?authSource=${cred.authSource}`;
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
787
903
|
//#region ../../packages/crypto/src/mysql.ts
|
|
788
904
|
/**
|
|
789
905
|
* Client-side construction of a MySQL/MariaDB `mysql_native_password`
|
|
@@ -1228,7 +1344,7 @@ function isServiceToken(value) {
|
|
|
1228
1344
|
}
|
|
1229
1345
|
//#endregion
|
|
1230
1346
|
//#region package.json
|
|
1231
|
-
var version = "0.
|
|
1347
|
+
var version = "0.16.0";
|
|
1232
1348
|
//#endregion
|
|
1233
1349
|
//#region ../../packages/api-client/src/index.ts
|
|
1234
1350
|
var SeekritApiError = class extends Error {
|
|
@@ -1301,6 +1417,15 @@ var SeekritClient = class {
|
|
|
1301
1417
|
listMembers(orgId) {
|
|
1302
1418
|
return this.request("GET", `/v1/orgs/${orgId}/members`);
|
|
1303
1419
|
}
|
|
1420
|
+
listInvites(orgId) {
|
|
1421
|
+
return this.request("GET", `/v1/orgs/${orgId}/invites`);
|
|
1422
|
+
}
|
|
1423
|
+
createInvite(orgId, input) {
|
|
1424
|
+
return this.request("POST", `/v1/orgs/${orgId}/invites`, input);
|
|
1425
|
+
}
|
|
1426
|
+
revokeInvite(orgId, inviteId) {
|
|
1427
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/invites/${inviteId}`);
|
|
1428
|
+
}
|
|
1304
1429
|
listApps(orgId) {
|
|
1305
1430
|
return this.request("GET", `/v1/orgs/${orgId}/apps`);
|
|
1306
1431
|
}
|
|
@@ -1677,7 +1802,7 @@ async function resolveGroup(ctx, opts) {
|
|
|
1677
1802
|
* `sts:AssumeRole` on the target role.
|
|
1678
1803
|
*/
|
|
1679
1804
|
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1680
|
-
function parseTtlSeconds$
|
|
1805
|
+
function parseTtlSeconds$6(input) {
|
|
1681
1806
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1682
1807
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1683
1808
|
return Number(m[1]) * ({
|
|
@@ -1717,7 +1842,7 @@ function registerAwsCommands(program) {
|
|
|
1717
1842
|
region: options.region,
|
|
1718
1843
|
...options.externalId ? { externalId: options.externalId } : {},
|
|
1719
1844
|
...sessionPolicy ? { sessionPolicy } : {},
|
|
1720
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$
|
|
1845
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
|
|
1721
1846
|
};
|
|
1722
1847
|
const baseCredential = resolveBaseCredential(options);
|
|
1723
1848
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
@@ -1765,7 +1890,7 @@ function registerAwsCommands(program) {
|
|
|
1765
1890
|
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1766
1891
|
const cfg = t.config;
|
|
1767
1892
|
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
1768
|
-
const ttlSeconds = parseTtlSeconds$
|
|
1893
|
+
const ttlSeconds = parseTtlSeconds$6(options.ttl);
|
|
1769
1894
|
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
1770
1895
|
const recipient = await generateAwsRecipientKeyPair();
|
|
1771
1896
|
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
@@ -1870,7 +1995,7 @@ function formatSecrets(values, format) {
|
|
|
1870
1995
|
* `roles/iam.serviceAccountTokenCreator` on the target.
|
|
1871
1996
|
*/
|
|
1872
1997
|
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1873
|
-
function parseTtlSeconds$
|
|
1998
|
+
function parseTtlSeconds$5(input) {
|
|
1874
1999
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1875
2000
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1876
2001
|
return Number(m[1]) * ({
|
|
@@ -1914,7 +2039,7 @@ function registerGcpCommands(program) {
|
|
|
1914
2039
|
serviceAccount: options.serviceAccount,
|
|
1915
2040
|
...options.scope?.length ? { scopes: options.scope } : {},
|
|
1916
2041
|
...options.delegate?.length ? { delegates: options.delegate } : {},
|
|
1917
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$
|
|
2042
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$5(options.maxTtl) } : {}
|
|
1918
2043
|
};
|
|
1919
2044
|
const keyJson = resolveServiceAccountKey(options);
|
|
1920
2045
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
@@ -1961,7 +2086,7 @@ function registerGcpCommands(program) {
|
|
|
1961
2086
|
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
1962
2087
|
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1963
2088
|
if (t.config.provider !== "gcp") fail(`"${t.name}" is not a gcp target (see \`seekrit aws\`)`);
|
|
1964
|
-
const ttlSeconds = parseTtlSeconds$
|
|
2089
|
+
const ttlSeconds = parseTtlSeconds$5(options.ttl);
|
|
1965
2090
|
if (ttlSeconds < 60) fail(`--ttl must be at least 60s`);
|
|
1966
2091
|
if (ttlSeconds > 43200) fail(`--ttl must be at most ${GCP_MAX_TTL_SECONDS / 3600}h`);
|
|
1967
2092
|
const recipient = await generateGcpRecipientKeyPair();
|
|
@@ -1996,6 +2121,139 @@ function registerGcpCommands(program) {
|
|
|
1996
2121
|
});
|
|
1997
2122
|
}
|
|
1998
2123
|
//#endregion
|
|
2124
|
+
//#region src/mongodb.ts
|
|
2125
|
+
/**
|
|
2126
|
+
* `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
|
|
2127
|
+
* secrets), the tier-2 sibling of `seekrit aws`.
|
|
2128
|
+
*
|
|
2129
|
+
* MongoDB hashes the password server-side (no verifier injection), so minting
|
|
2130
|
+
* generates an ephemeral P-256 keypair on THIS machine and sends only the public
|
|
2131
|
+
* key; the broker creates the user and returns the credential wrapped to that
|
|
2132
|
+
* key, so the control plane only ever relays ciphertext and only this machine
|
|
2133
|
+
* can unwrap it. Registering a target wraps the admin connection string to the
|
|
2134
|
+
* broker's public key locally, so the control plane never sees it either.
|
|
2135
|
+
*/
|
|
2136
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
2137
|
+
function parseTtlSeconds$4(input) {
|
|
2138
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
2139
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 8h)`);
|
|
2140
|
+
return Number(m[1]) * ({
|
|
2141
|
+
s: 1,
|
|
2142
|
+
m: 60,
|
|
2143
|
+
h: 3600,
|
|
2144
|
+
d: 86400
|
|
2145
|
+
}[m[2] || "s"] ?? 1);
|
|
2146
|
+
}
|
|
2147
|
+
/** Collect a repeatable option into an array. */
|
|
2148
|
+
function collect$4(value, previous) {
|
|
2149
|
+
return [...previous, value];
|
|
2150
|
+
}
|
|
2151
|
+
/** Parse `readWrite@app` → { role, db } for a custom target. */
|
|
2152
|
+
function parseRole(spec) {
|
|
2153
|
+
const [role, db] = spec.split("@");
|
|
2154
|
+
if (!role || !db) fail(`invalid --role "${spec}" (use role@database, e.g. readWrite@app)`);
|
|
2155
|
+
return {
|
|
2156
|
+
role,
|
|
2157
|
+
db
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* The admin connection string the broker provisions with. From `--uri` or
|
|
2162
|
+
* SEEKRIT_MONGODB_ADMIN_URL; never leaves this machine unwrapped — it is wrapped
|
|
2163
|
+
* to the broker key. Needs `userAdmin` on the target database.
|
|
2164
|
+
*/
|
|
2165
|
+
function resolveAdminUri(uri) {
|
|
2166
|
+
const value = uri ?? process.env.SEEKRIT_MONGODB_ADMIN_URL;
|
|
2167
|
+
if (!value) fail("provide the admin connection string via --uri or SEEKRIT_MONGODB_ADMIN_URL (mongodb://user:pass@host:port/?authSource=admin; needs userAdmin on the database)");
|
|
2168
|
+
return value;
|
|
2169
|
+
}
|
|
2170
|
+
function registerMongoCommands(program) {
|
|
2171
|
+
const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
|
|
2172
|
+
const target = mongo.command("target").description("manage MongoDB targets");
|
|
2173
|
+
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
|
|
2174
|
+
const ctx = buildContext();
|
|
2175
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2176
|
+
const adminUri = resolveAdminUri(options.uri);
|
|
2177
|
+
const url = new URL(adminUri);
|
|
2178
|
+
const accessLevel = options.access;
|
|
2179
|
+
if (accessLevel === "custom" && options.role.length === 0) fail("--access custom requires at least one --role role@db");
|
|
2180
|
+
const config = {
|
|
2181
|
+
provider: "mongodb",
|
|
2182
|
+
executor: "in_do",
|
|
2183
|
+
accessLevel,
|
|
2184
|
+
connection: {
|
|
2185
|
+
host: url.hostname,
|
|
2186
|
+
port: url.port ? Number(url.port) : 27017,
|
|
2187
|
+
database: options.database
|
|
2188
|
+
},
|
|
2189
|
+
...options.authSource ? { authSource: options.authSource } : {},
|
|
2190
|
+
...accessLevel === "custom" ? { roles: options.role.map(parseRole) } : {},
|
|
2191
|
+
...options.tls ? {} : { tls: false }
|
|
2192
|
+
};
|
|
2193
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
2194
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminUri), publicKeyJwk);
|
|
2195
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
2196
|
+
name: options.name,
|
|
2197
|
+
config,
|
|
2198
|
+
wrappedAdminSecret
|
|
2199
|
+
});
|
|
2200
|
+
console.error(`registered MongoDB target ${created.name} (${created.id})`);
|
|
2201
|
+
console.error("\nEnsure a provisioning user exists, then `seekrit mongodb lease`:\n");
|
|
2202
|
+
console.log(mongoAdminSetupInstructions(config));
|
|
2203
|
+
});
|
|
2204
|
+
target.command("list").description("list MongoDB targets").option("--org <slug>").action(async (options) => {
|
|
2205
|
+
const ctx = buildContext();
|
|
2206
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2207
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2208
|
+
for (const t of targets) {
|
|
2209
|
+
const cfg = t.config;
|
|
2210
|
+
if (cfg.provider !== "mongodb") continue;
|
|
2211
|
+
const host = `${cfg.connection.host}:${cfg.connection.port}`;
|
|
2212
|
+
console.log(`${t.id}\t${t.name}\t${host}\t${cfg.connection.database}\t${cfg.accessLevel ?? "readonly"}`);
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>").action(async (targetId, options) => {
|
|
2216
|
+
const ctx = buildContext();
|
|
2217
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2218
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
2219
|
+
console.error(`deleted ${targetId}`);
|
|
2220
|
+
});
|
|
2221
|
+
mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
2222
|
+
const ctx = buildContext();
|
|
2223
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2224
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2225
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
2226
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
2227
|
+
if (t.config.provider !== "mongodb") fail(`"${t?.name}" is not a mongodb target (see \`seekrit aws\`)`);
|
|
2228
|
+
const recipient = await generateMongoRecipientKeyPair();
|
|
2229
|
+
const { mongodb: leased } = await ctx.client.mintLease(org.id, {
|
|
2230
|
+
provider: "mongodb",
|
|
2231
|
+
targetId: t.id,
|
|
2232
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
2233
|
+
ttlSeconds: parseTtlSeconds$4(options.ttl)
|
|
2234
|
+
});
|
|
2235
|
+
const cred = await unwrapMongoCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
2236
|
+
console.error(`leased ${cred.username} on ${cred.host}:${cred.port}/${cred.database} — expires ${cred.expiration}`);
|
|
2237
|
+
if (options.json) console.log(JSON.stringify(cred, null, 2));
|
|
2238
|
+
else console.log(`export MONGODB_URI='${cred.uri}'`);
|
|
2239
|
+
});
|
|
2240
|
+
mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
2241
|
+
const ctx = buildContext();
|
|
2242
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2243
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
2244
|
+
for (const l of leases) {
|
|
2245
|
+
if (l.provider !== "mongodb") continue;
|
|
2246
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
2250
|
+
const ctx = buildContext();
|
|
2251
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2252
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
2253
|
+
console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
//#endregion
|
|
1999
2257
|
//#region src/provisioner.ts
|
|
2000
2258
|
/**
|
|
2001
2259
|
* `seekrit provisioner` — helpers for the self-hosted **remote executor**
|
|
@@ -3087,6 +3345,7 @@ registerProvisionerCommands(program);
|
|
|
3087
3345
|
registerSshCommands(program);
|
|
3088
3346
|
registerAwsCommands(program);
|
|
3089
3347
|
registerGcpCommands(program);
|
|
3348
|
+
registerMongoCommands(program);
|
|
3090
3349
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
3091
3350
|
const { runMcpServer } = await import("./mcp-ARBuneH3.js");
|
|
3092
3351
|
await runMcpServer();
|