@seekrit/cli 0.14.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 +502 -7
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -12,7 +12,9 @@ z.enum([
|
|
|
12
12
|
"mysql",
|
|
13
13
|
"ssh",
|
|
14
14
|
"redis",
|
|
15
|
-
"aws"
|
|
15
|
+
"aws",
|
|
16
|
+
"gcp",
|
|
17
|
+
"mongodb"
|
|
16
18
|
]);
|
|
17
19
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
18
20
|
/**
|
|
@@ -83,6 +85,22 @@ const awsRegionSchema = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be a
|
|
|
83
85
|
const awsExternalIdSchema = z.string().regex(/^[\w+=,.@:/-]{2,1224}$/, "must be a valid STS external id");
|
|
84
86
|
z.string().regex(/^[\w+=,.@-]{2,64}$/, "must be 2–64 chars of [A-Za-z0-9_+=,.@-]");
|
|
85
87
|
/**
|
|
88
|
+
* A GCP service-account email the broker is allowed to impersonate (or that
|
|
89
|
+
* appears in a delegation chain). Structurally validated and bounded: it is
|
|
90
|
+
* interpolated into the IAM Credentials API URL path, so the charset excludes
|
|
91
|
+
* anything that could break out of a path segment. Covers user-managed
|
|
92
|
+
* (`name@<project>.iam.gserviceaccount.com`) and Google-managed
|
|
93
|
+
* (`<project-number>-compute@developer.gserviceaccount.com`) forms.
|
|
94
|
+
*/
|
|
95
|
+
const gcpServiceAccountEmailSchema = z.string().max(256).regex(/^[a-z0-9-]+@[a-z0-9.-]+\.gserviceaccount\.com$/, "must be a service-account email (…@….gserviceaccount.com)");
|
|
96
|
+
/**
|
|
97
|
+
* An OAuth 2.0 scope granted to the minted access token, e.g.
|
|
98
|
+
* `https://www.googleapis.com/auth/cloud-platform`. Bounded and whitespace-free
|
|
99
|
+
* (scopes are space-delimited); passed to the IAM Credentials API in a JSON body
|
|
100
|
+
* array, not a URL, so this is sanity/DoS hardening rather than an injection gate.
|
|
101
|
+
*/
|
|
102
|
+
const gcpOauthScopeSchema = z.string().min(1).max(256).regex(/^\S+$/, "must be a single OAuth scope with no whitespace");
|
|
103
|
+
/**
|
|
86
104
|
* The consumer's ephemeral P-256 public key (JWK-serialized) that a tier-2
|
|
87
105
|
* credential is wrapped to before it is returned. Validated structurally here;
|
|
88
106
|
* the executor imports it defensively before wrapping. Bounded so a giant blob
|
|
@@ -96,6 +114,22 @@ const p256PublicKeyJwkSchema = z.string().max(2048).refine((s) => {
|
|
|
96
114
|
return false;
|
|
97
115
|
}
|
|
98
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
|
+
});
|
|
99
133
|
const postgresAccessLevelSchema = z.enum([
|
|
100
134
|
"readonly",
|
|
101
135
|
"readwrite",
|
|
@@ -116,6 +150,11 @@ const redisAccessLevelSchema = z.enum([
|
|
|
116
150
|
"readwrite",
|
|
117
151
|
"custom"
|
|
118
152
|
]);
|
|
153
|
+
const mongoAccessLevelSchema = z.enum([
|
|
154
|
+
"readonly",
|
|
155
|
+
"readwrite",
|
|
156
|
+
"custom"
|
|
157
|
+
]);
|
|
119
158
|
const connectionSchema = z.object({
|
|
120
159
|
host: z.string().min(1),
|
|
121
160
|
port: z.number().int().min(1).max(65535),
|
|
@@ -184,12 +223,33 @@ const awsTargetConfigSchema = z.object({
|
|
|
184
223
|
sessionPolicy: z.string().min(1).max(4e3).optional(),
|
|
185
224
|
maxTtlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS).optional()
|
|
186
225
|
});
|
|
226
|
+
const GCP_MAX_TTL_SECONDS = 3600 * 12;
|
|
227
|
+
const gcpTargetConfigSchema = z.object({
|
|
228
|
+
provider: z.literal("gcp"),
|
|
229
|
+
executor: z.literal("in_do"),
|
|
230
|
+
serviceAccount: gcpServiceAccountEmailSchema,
|
|
231
|
+
scopes: z.array(gcpOauthScopeSchema).min(1).max(32).optional(),
|
|
232
|
+
delegates: z.array(gcpServiceAccountEmailSchema).max(8).optional(),
|
|
233
|
+
maxTtlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
|
|
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
|
+
});
|
|
187
245
|
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
188
246
|
postgresTargetConfigSchema,
|
|
189
247
|
mysqlTargetConfigSchema,
|
|
190
248
|
redisTargetConfigSchema,
|
|
191
249
|
sshTargetConfigSchema,
|
|
192
|
-
awsTargetConfigSchema
|
|
250
|
+
awsTargetConfigSchema,
|
|
251
|
+
gcpTargetConfigSchema,
|
|
252
|
+
mongoTargetConfigSchema
|
|
193
253
|
]);
|
|
194
254
|
z.object({
|
|
195
255
|
name: z.string().trim().min(1).max(128),
|
|
@@ -266,12 +326,43 @@ const mintAwsLeaseSchema = z.object({
|
|
|
266
326
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
267
327
|
ttlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS)
|
|
268
328
|
});
|
|
329
|
+
/**
|
|
330
|
+
* Client → API: mint a GCP lease. Like AWS (tier 2): the client generates an
|
|
331
|
+
* ephemeral P-256 keypair locally and sends only the public key; the IAM
|
|
332
|
+
* Credentials API mints the access token and the broker returns it wrapped to
|
|
333
|
+
* that key. The private key never leaves the requesting machine, so the plaintext
|
|
334
|
+
* token is only decryptable there.
|
|
335
|
+
*
|
|
336
|
+
* TTL bounds are GCP's `generateAccessToken` limits (1 min – 12 h); tokens over
|
|
337
|
+
* 1 h require the credential-lifetime-extension org policy.
|
|
338
|
+
*/
|
|
339
|
+
const mintGcpLeaseSchema = z.object({
|
|
340
|
+
provider: z.literal("gcp"),
|
|
341
|
+
targetId: z.string().min(1),
|
|
342
|
+
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
343
|
+
ttlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
|
|
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
|
+
});
|
|
269
358
|
z.discriminatedUnion("provider", [
|
|
270
359
|
mintPostgresLeaseSchema,
|
|
271
360
|
mintMysqlLeaseSchema,
|
|
272
361
|
mintRedisLeaseSchema,
|
|
273
362
|
mintSshLeaseSchema,
|
|
274
|
-
mintAwsLeaseSchema
|
|
363
|
+
mintAwsLeaseSchema,
|
|
364
|
+
mintGcpLeaseSchema,
|
|
365
|
+
mintMongoLeaseSchema
|
|
275
366
|
]);
|
|
276
367
|
//#endregion
|
|
277
368
|
//#region ../../packages/core/src/providers/aws.ts
|
|
@@ -302,6 +393,55 @@ function awsTrustPolicyInstructions(config) {
|
|
|
302
393
|
].join("\n");
|
|
303
394
|
}
|
|
304
395
|
//#endregion
|
|
396
|
+
//#region ../../packages/core/src/providers/gcp.ts
|
|
397
|
+
/**
|
|
398
|
+
* The one-time IAM setup an admin performs so seekrit can impersonate the target
|
|
399
|
+
* service account. Analogue of `awsTrustPolicyInstructions` — printed for the
|
|
400
|
+
* admin to apply, never executed by seekrit. Grants the *source* service account
|
|
401
|
+
* (the one whose key was registered as the admin secret) the token-creator role
|
|
402
|
+
* on the target service account.
|
|
403
|
+
*/
|
|
404
|
+
function gcpSetupInstructions(config) {
|
|
405
|
+
return [
|
|
406
|
+
"# Grant the service account whose key you registered as the admin secret",
|
|
407
|
+
"# permission to mint tokens for the target service account. Replace",
|
|
408
|
+
"# <SOURCE_SA_EMAIL> with the client_email from that key JSON.",
|
|
409
|
+
`gcloud iam service-accounts add-iam-policy-binding ${config.serviceAccount} \\`,
|
|
410
|
+
" --member=\"serviceAccount:<SOURCE_SA_EMAIL>\" \\",
|
|
411
|
+
" --role=\"roles/iam.serviceAccountTokenCreator\"",
|
|
412
|
+
"",
|
|
413
|
+
"# The source service account's project must have the IAM Service Account",
|
|
414
|
+
"# Credentials API enabled (seekrit only ever calls generateAccessToken):",
|
|
415
|
+
"gcloud services enable iamcredentials.googleapis.com"
|
|
416
|
+
].join("\n");
|
|
417
|
+
}
|
|
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
|
|
305
445
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
306
446
|
/**
|
|
307
447
|
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
@@ -378,6 +518,8 @@ const NOTIFICATION_TYPES = [
|
|
|
378
518
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
379
519
|
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
380
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));
|
|
381
523
|
/** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
|
|
382
524
|
const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
|
|
383
525
|
z.enum([
|
|
@@ -385,6 +527,8 @@ z.enum([
|
|
|
385
527
|
"admin",
|
|
386
528
|
"member"
|
|
387
529
|
]);
|
|
530
|
+
/** Role a person may be invited at — never `owner` (ownership isn't invitable). */
|
|
531
|
+
const inviteRoleSchema = z.enum(["admin", "member"]);
|
|
388
532
|
const principalTypeSchema = z.enum(["user", "service_token"]);
|
|
389
533
|
/** Org-level capability a service token can hold (never `owner`). */
|
|
390
534
|
const serviceTokenRoleSchema = z.enum(["admin", "member"]);
|
|
@@ -396,6 +540,10 @@ z.object({
|
|
|
396
540
|
name: nameSchema,
|
|
397
541
|
slug: slugSchema
|
|
398
542
|
});
|
|
543
|
+
z.object({
|
|
544
|
+
email: emailSchema,
|
|
545
|
+
role: inviteRoleSchema.default("member")
|
|
546
|
+
});
|
|
399
547
|
z.object({
|
|
400
548
|
name: nameSchema,
|
|
401
549
|
slug: slugSchema
|
|
@@ -690,6 +838,68 @@ async function unwrapAwsCredential(wrapped, privateKeyJwk) {
|
|
|
690
838
|
};
|
|
691
839
|
}
|
|
692
840
|
//#endregion
|
|
841
|
+
//#region ../../packages/crypto/src/gcp.ts
|
|
842
|
+
/** Generate the ephemeral P-256 keypair a client uses to receive one GCP lease. */
|
|
843
|
+
async function generateGcpRecipientKeyPair() {
|
|
844
|
+
return generateKeyPair();
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Unwrap the `wd1.` blob returned by a GCP lease mint using the ephemeral
|
|
848
|
+
* private key generated for that lease, yielding the plaintext access token.
|
|
849
|
+
*/
|
|
850
|
+
async function unwrapGcpCredential(wrapped, privateKeyJwk) {
|
|
851
|
+
const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
|
|
852
|
+
let parsed;
|
|
853
|
+
try {
|
|
854
|
+
parsed = JSON.parse(utf8Decode(bytes));
|
|
855
|
+
} catch {
|
|
856
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential was not valid JSON");
|
|
857
|
+
}
|
|
858
|
+
const c = parsed;
|
|
859
|
+
if (typeof c.accessToken !== "string" || typeof c.expiration !== "string" || typeof c.serviceAccount !== "string" || !Array.isArray(c.scopes) || !c.scopes.every((s) => typeof s === "string")) throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential is missing fields");
|
|
860
|
+
return {
|
|
861
|
+
accessToken: c.accessToken,
|
|
862
|
+
expiration: c.expiration,
|
|
863
|
+
serviceAccount: c.serviceAccount,
|
|
864
|
+
scopes: c.scopes
|
|
865
|
+
};
|
|
866
|
+
}
|
|
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
|
|
693
903
|
//#region ../../packages/crypto/src/mysql.ts
|
|
694
904
|
/**
|
|
695
905
|
* Client-side construction of a MySQL/MariaDB `mysql_native_password`
|
|
@@ -1134,7 +1344,7 @@ function isServiceToken(value) {
|
|
|
1134
1344
|
}
|
|
1135
1345
|
//#endregion
|
|
1136
1346
|
//#region package.json
|
|
1137
|
-
var version = "0.
|
|
1347
|
+
var version = "0.16.0";
|
|
1138
1348
|
//#endregion
|
|
1139
1349
|
//#region ../../packages/api-client/src/index.ts
|
|
1140
1350
|
var SeekritApiError = class extends Error {
|
|
@@ -1207,6 +1417,15 @@ var SeekritClient = class {
|
|
|
1207
1417
|
listMembers(orgId) {
|
|
1208
1418
|
return this.request("GET", `/v1/orgs/${orgId}/members`);
|
|
1209
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
|
+
}
|
|
1210
1429
|
listApps(orgId) {
|
|
1211
1430
|
return this.request("GET", `/v1/orgs/${orgId}/apps`);
|
|
1212
1431
|
}
|
|
@@ -1583,7 +1802,7 @@ async function resolveGroup(ctx, opts) {
|
|
|
1583
1802
|
* `sts:AssumeRole` on the target role.
|
|
1584
1803
|
*/
|
|
1585
1804
|
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1586
|
-
function parseTtlSeconds$
|
|
1805
|
+
function parseTtlSeconds$6(input) {
|
|
1587
1806
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1588
1807
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1589
1808
|
return Number(m[1]) * ({
|
|
@@ -1623,7 +1842,7 @@ function registerAwsCommands(program) {
|
|
|
1623
1842
|
region: options.region,
|
|
1624
1843
|
...options.externalId ? { externalId: options.externalId } : {},
|
|
1625
1844
|
...sessionPolicy ? { sessionPolicy } : {},
|
|
1626
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$
|
|
1845
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
|
|
1627
1846
|
};
|
|
1628
1847
|
const baseCredential = resolveBaseCredential(options);
|
|
1629
1848
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
@@ -1671,7 +1890,7 @@ function registerAwsCommands(program) {
|
|
|
1671
1890
|
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1672
1891
|
const cfg = t.config;
|
|
1673
1892
|
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
1674
|
-
const ttlSeconds = parseTtlSeconds$
|
|
1893
|
+
const ttlSeconds = parseTtlSeconds$6(options.ttl);
|
|
1675
1894
|
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
1676
1895
|
const recipient = await generateAwsRecipientKeyPair();
|
|
1677
1896
|
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
@@ -1761,6 +1980,280 @@ function formatSecrets(values, format) {
|
|
|
1761
1980
|
}
|
|
1762
1981
|
}
|
|
1763
1982
|
//#endregion
|
|
1983
|
+
//#region src/gcp.ts
|
|
1984
|
+
/**
|
|
1985
|
+
* `seekrit gcp` — temporary GCP credentials via IAM Credentials
|
|
1986
|
+
* `generateAccessToken` (Vault-style dynamic secrets, the tier-2 sibling of
|
|
1987
|
+
* `seekrit aws`).
|
|
1988
|
+
*
|
|
1989
|
+
* Zero-knowledge for the leased credential: minting generates an ephemeral P-256
|
|
1990
|
+
* keypair on THIS machine and sends only the public key; GCP mints the token and
|
|
1991
|
+
* the broker returns it wrapped to that key, so the control plane only ever
|
|
1992
|
+
* relays ciphertext and only this machine can unwrap it. Registering a target
|
|
1993
|
+
* wraps the service-account key JSON to the broker's public key locally, so the
|
|
1994
|
+
* control plane never sees it either — the source service account needs only
|
|
1995
|
+
* `roles/iam.serviceAccountTokenCreator` on the target.
|
|
1996
|
+
*/
|
|
1997
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1998
|
+
function parseTtlSeconds$5(input) {
|
|
1999
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
2000
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
2001
|
+
return Number(m[1]) * ({
|
|
2002
|
+
s: 1,
|
|
2003
|
+
m: 60,
|
|
2004
|
+
h: 3600,
|
|
2005
|
+
d: 86400
|
|
2006
|
+
}[m[2] || "s"] ?? 1);
|
|
2007
|
+
}
|
|
2008
|
+
/** Collect a repeatable flag (e.g. --scope) into a list. */
|
|
2009
|
+
function collectList$1(value, acc = []) {
|
|
2010
|
+
acc.push(value);
|
|
2011
|
+
return acc;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* The service-account key JSON the broker impersonates with. From --key-file or
|
|
2015
|
+
* GOOGLE_APPLICATION_CREDENTIALS. Never leaves this machine unwrapped — it is
|
|
2016
|
+
* wrapped to the broker key before upload.
|
|
2017
|
+
*/
|
|
2018
|
+
function resolveServiceAccountKey(opts) {
|
|
2019
|
+
const path = opts.keyFile ?? process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
2020
|
+
if (!path) fail("provide the source service-account key JSON via --key-file or GOOGLE_APPLICATION_CREDENTIALS (it needs roles/iam.serviceAccountTokenCreator on the target)");
|
|
2021
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
2022
|
+
try {
|
|
2023
|
+
const parsed = JSON.parse(raw);
|
|
2024
|
+
if (typeof parsed.client_email !== "string" || typeof parsed.private_key !== "string") fail(`${path} is not a service-account key JSON (missing client_email/private_key)`);
|
|
2025
|
+
} catch {
|
|
2026
|
+
fail(`${path} is not valid JSON`);
|
|
2027
|
+
}
|
|
2028
|
+
return raw;
|
|
2029
|
+
}
|
|
2030
|
+
function registerGcpCommands(program) {
|
|
2031
|
+
const gcp = program.command("gcp").description("temporary GCP credentials (IAM generateAccessToken, zero-knowledge)");
|
|
2032
|
+
const target = gcp.command("target").description("manage GCP service-account targets");
|
|
2033
|
+
target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
|
|
2034
|
+
const ctx = buildContext();
|
|
2035
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2036
|
+
const config = {
|
|
2037
|
+
provider: "gcp",
|
|
2038
|
+
executor: "in_do",
|
|
2039
|
+
serviceAccount: options.serviceAccount,
|
|
2040
|
+
...options.scope?.length ? { scopes: options.scope } : {},
|
|
2041
|
+
...options.delegate?.length ? { delegates: options.delegate } : {},
|
|
2042
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$5(options.maxTtl) } : {}
|
|
2043
|
+
};
|
|
2044
|
+
const keyJson = resolveServiceAccountKey(options);
|
|
2045
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
2046
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(keyJson), publicKeyJwk);
|
|
2047
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
2048
|
+
name: options.name,
|
|
2049
|
+
config,
|
|
2050
|
+
wrappedAdminSecret
|
|
2051
|
+
});
|
|
2052
|
+
console.error(`registered GCP target ${created.name} (${created.id})`);
|
|
2053
|
+
console.error("\nGrant the source SA the token-creator role, then `seekrit gcp lease`:\n");
|
|
2054
|
+
console.log(gcpSetupInstructions(config));
|
|
2055
|
+
});
|
|
2056
|
+
target.command("list").description("list GCP service-account targets").option("--org <slug>").action(async (options) => {
|
|
2057
|
+
const ctx = buildContext();
|
|
2058
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2059
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2060
|
+
for (const t of targets) {
|
|
2061
|
+
const cfg = t.config;
|
|
2062
|
+
if (cfg.provider !== "gcp") continue;
|
|
2063
|
+
console.log(`${t.id}\t${t.name}\t${cfg.serviceAccount}`);
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>").action(async (targetId, options) => {
|
|
2067
|
+
const ctx = buildContext();
|
|
2068
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2069
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2070
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
2071
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
2072
|
+
const cfg = t.config;
|
|
2073
|
+
if (cfg.provider !== "gcp") fail("not a gcp target (see `seekrit aws`/`seekrit ssh`)");
|
|
2074
|
+
console.log(gcpSetupInstructions(cfg));
|
|
2075
|
+
});
|
|
2076
|
+
target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>").action(async (targetId, options) => {
|
|
2077
|
+
const ctx = buildContext();
|
|
2078
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2079
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
2080
|
+
console.error(`deleted ${targetId}`);
|
|
2081
|
+
});
|
|
2082
|
+
gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
2083
|
+
const ctx = buildContext();
|
|
2084
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2085
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2086
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
2087
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
2088
|
+
if (t.config.provider !== "gcp") fail(`"${t.name}" is not a gcp target (see \`seekrit aws\`)`);
|
|
2089
|
+
const ttlSeconds = parseTtlSeconds$5(options.ttl);
|
|
2090
|
+
if (ttlSeconds < 60) fail(`--ttl must be at least 60s`);
|
|
2091
|
+
if (ttlSeconds > 43200) fail(`--ttl must be at most ${GCP_MAX_TTL_SECONDS / 3600}h`);
|
|
2092
|
+
const recipient = await generateGcpRecipientKeyPair();
|
|
2093
|
+
const { gcp: leased } = await ctx.client.mintLease(org.id, {
|
|
2094
|
+
provider: "gcp",
|
|
2095
|
+
targetId: t.id,
|
|
2096
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
2097
|
+
ttlSeconds
|
|
2098
|
+
});
|
|
2099
|
+
const cred = await unwrapGcpCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
2100
|
+
console.error(`leased ${cred.serviceAccount} — expires ${cred.expiration}`);
|
|
2101
|
+
if (options.json) console.log(JSON.stringify(cred, null, 2));
|
|
2102
|
+
else {
|
|
2103
|
+
console.log(`export CLOUDSDK_AUTH_ACCESS_TOKEN=${cred.accessToken}`);
|
|
2104
|
+
console.log(`export GOOGLE_OAUTH_ACCESS_TOKEN=${cred.accessToken}`);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
2108
|
+
const ctx = buildContext();
|
|
2109
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2110
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
2111
|
+
for (const l of leases) {
|
|
2112
|
+
if (l.provider !== "gcp") continue;
|
|
2113
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
2117
|
+
const ctx = buildContext();
|
|
2118
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2119
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
2120
|
+
console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
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
|
|
1764
2257
|
//#region src/provisioner.ts
|
|
1765
2258
|
/**
|
|
1766
2259
|
* `seekrit provisioner` — helpers for the self-hosted **remote executor**
|
|
@@ -2851,6 +3344,8 @@ registerRedisCommands(program);
|
|
|
2851
3344
|
registerProvisionerCommands(program);
|
|
2852
3345
|
registerSshCommands(program);
|
|
2853
3346
|
registerAwsCommands(program);
|
|
3347
|
+
registerGcpCommands(program);
|
|
3348
|
+
registerMongoCommands(program);
|
|
2854
3349
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
2855
3350
|
const { runMcpServer } = await import("./mcp-ARBuneH3.js");
|
|
2856
3351
|
await runMcpServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^26.1.0",
|
|
25
25
|
"tsdown": "^0.22.3",
|
|
26
|
-
"@seekrit/api-client": "0.0.1",
|
|
27
26
|
"@seekrit/core": "0.0.1",
|
|
28
|
-
"@seekrit/crypto": "0.0.1"
|
|
27
|
+
"@seekrit/crypto": "0.0.1",
|
|
28
|
+
"@seekrit/api-client": "0.0.1"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsdown",
|