@seekrit/cli 2.7.1 → 2.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 +1055 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -169,7 +169,7 @@ async function policySignerThumbprint(jwk) {
|
|
|
169
169
|
x: jwk.x,
|
|
170
170
|
y: jwk.y
|
|
171
171
|
});
|
|
172
|
-
const digest = await crypto.subtle.digest("SHA-256", utf8(canonical));
|
|
172
|
+
const digest = await crypto.subtle.digest("SHA-256", utf8$1(canonical));
|
|
173
173
|
return base64url$1(new Uint8Array(digest));
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
@@ -184,7 +184,7 @@ async function policySignerThumbprint(jwk) {
|
|
|
184
184
|
*/
|
|
185
185
|
async function signAgentPolicy(privateKey, signerJwk, draft) {
|
|
186
186
|
const kid = await policySignerThumbprint(signerJwk);
|
|
187
|
-
const bytes = utf8(canonicalizeAgentPolicy({
|
|
187
|
+
const bytes = utf8$1(canonicalizeAgentPolicy({
|
|
188
188
|
...draft,
|
|
189
189
|
signer: {
|
|
190
190
|
kid,
|
|
@@ -380,7 +380,7 @@ function normalizeRule(rule) {
|
|
|
380
380
|
label: rule.label?.trim() || void 0
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
|
-
function utf8(text) {
|
|
383
|
+
function utf8$1(text) {
|
|
384
384
|
return new TextEncoder().encode(text);
|
|
385
385
|
}
|
|
386
386
|
function utf8Decode$1(bytes) {
|
|
@@ -1370,6 +1370,16 @@ z$1.object({
|
|
|
1370
1370
|
body: z$1.string().max(65536).optional()
|
|
1371
1371
|
}).strict().refine((v) => !["GET", "HEAD"].includes(v.method) || v.body === void 0, "GET and HEAD cannot carry a body");
|
|
1372
1372
|
z$1.object({ audience: audienceSchema }).strict();
|
|
1373
|
+
// ../../packages/core/src/customer-provisioner.ts
|
|
1374
|
+
/** A credential shown to the administrator for the provisioner deployment.
|
|
1375
|
+
* Routing metadata is bundled with its signing key; upstream secrets stay local. */
|
|
1376
|
+
function createProvisionerConnection(input) {
|
|
1377
|
+
const body = JSON.stringify({
|
|
1378
|
+
version: 1,
|
|
1379
|
+
...input
|
|
1380
|
+
});
|
|
1381
|
+
return `sp1.${btoa(String.fromCharCode(...new TextEncoder().encode(body))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
1382
|
+
}
|
|
1373
1383
|
// ../../packages/core/src/dotenv.ts
|
|
1374
1384
|
/**
|
|
1375
1385
|
* `.env` parsing and serialization — the canonical definition of what a `.env`
|
|
@@ -1752,10 +1762,6 @@ function prettyJsonOrRaw(value) {
|
|
|
1752
1762
|
const inspection = inspectJsonValue(value);
|
|
1753
1763
|
return inspection.kind === "json" ? inspection.pretty : value;
|
|
1754
1764
|
}
|
|
1755
|
-
// ../../packages/core/src/managed-providers/aws-cache.ts
|
|
1756
|
-
const cacheRegionSchema = z$1.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/);
|
|
1757
|
-
const cacheGroupSchema = z$1.string().min(1).max(128).regex(/^[a-zA-Z][a-zA-Z0-9-]*$/);
|
|
1758
|
-
const cacheAccessSchema = z$1.string().min(1).max(4096).refine((value) => /^on(?:\s|$)/.test(value) && !/(?:^|\s)(?:nopass|resetpass|[>#<])/.test(value), "ACL must enable the user and must not contain password directives");
|
|
1759
1765
|
// ../../packages/core/src/managed-providers/common.ts
|
|
1760
1766
|
const managedEndpointSchema = z$1.string().url().max(2048).refine((value) => {
|
|
1761
1767
|
try {
|
|
@@ -1768,6 +1774,10 @@ const managedEndpointSchema = z$1.string().url().max(2048).refine((value) => {
|
|
|
1768
1774
|
}, "use a public HTTPS endpoint without embedded credentials, query, or fragment");
|
|
1769
1775
|
const managedTtlSchema = z$1.number().int().min(60).max(604800).optional();
|
|
1770
1776
|
const nameSchema$1 = z$1.string().min(1).max(128).regex(/^[a-zA-Z_][a-zA-Z0-9_-]*$/);
|
|
1777
|
+
// ../../packages/core/src/managed-providers/aws-cache.ts
|
|
1778
|
+
const cacheRegionSchema = z$1.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/);
|
|
1779
|
+
const cacheGroupSchema = z$1.string().min(1).max(128).regex(/^[a-zA-Z][a-zA-Z0-9-]*$/);
|
|
1780
|
+
const cacheAccessSchema = z$1.string().min(1).max(4096).refine((value) => /^on(?:\s|$)/.test(value) && !/(?:^|\s)(?:nopass|resetpass|[>#<])/.test(value), "ACL must enable the user and must not contain password directives");
|
|
1771
1781
|
// ../../packages/core/src/managed-providers/memorydb.ts
|
|
1772
1782
|
const memorydbTargetConfigSchema = z$1.object({
|
|
1773
1783
|
provider: z$1.literal("memorydb"),
|
|
@@ -1956,7 +1966,7 @@ const tailscaleTargetConfigSchema = z$1.object({
|
|
|
1956
1966
|
message: "federation requires HTTPS issuer, audience, and subject"
|
|
1957
1967
|
});
|
|
1958
1968
|
});
|
|
1959
|
-
const
|
|
1969
|
+
const hostedManagedTargetConfigSchema = z$1.discriminatedUnion("provider", [
|
|
1960
1970
|
clickhouseTargetConfigSchema,
|
|
1961
1971
|
rabbitmqTargetConfigSchema,
|
|
1962
1972
|
milvusTargetConfigSchema,
|
|
@@ -1969,12 +1979,230 @@ const managedTargetConfigSchema = z$1.discriminatedUnion("provider", [
|
|
|
1969
1979
|
tailscaleTargetConfigSchema,
|
|
1970
1980
|
elasticsearchTargetConfigSchema
|
|
1971
1981
|
]);
|
|
1982
|
+
function hasLocalServiceEndpoint(provider) {
|
|
1983
|
+
return [
|
|
1984
|
+
"milvus",
|
|
1985
|
+
"rabbitmq",
|
|
1986
|
+
"clickhouse",
|
|
1987
|
+
"elasticsearch"
|
|
1988
|
+
].includes(provider);
|
|
1989
|
+
}
|
|
1990
|
+
/** Reuse the provider's existing permission validation without accepting a
|
|
1991
|
+
* remotely supplied destination for a locally held administrator credential. */
|
|
1992
|
+
function customerSettingsAsHosted(provider, settings) {
|
|
1993
|
+
const endpoint = typeof settings.endpoint === "string" ? settings.endpoint : void 0;
|
|
1994
|
+
if (hasLocalServiceEndpoint(provider) && endpoint !== void 0) {
|
|
1995
|
+
const url = new URL(z$1.string().parse(endpoint));
|
|
1996
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Use an HTTPS service endpoint without credentials.");
|
|
1997
|
+
}
|
|
1998
|
+
const normalized = hostedManagedTargetConfigSchema.parse({
|
|
1999
|
+
...settings,
|
|
2000
|
+
provider,
|
|
2001
|
+
executor: "in_do",
|
|
2002
|
+
...hasLocalServiceEndpoint(provider) ? { endpoint: "https://customer-service.example.com" } : {}
|
|
2003
|
+
});
|
|
2004
|
+
return endpoint === void 0 ? normalized : {
|
|
2005
|
+
...normalized,
|
|
2006
|
+
endpoint
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
/** Permissions remain configured in Seekrit; destination and admin secret stay local. */
|
|
2010
|
+
const remoteManagedTargetConfigSchema = z$1.object({
|
|
2011
|
+
provider: z$1.enum(MANAGED_LEASE_KINDS),
|
|
2012
|
+
executor: z$1.literal("remote"),
|
|
2013
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2014
|
+
transport: z$1.enum(["push", "pull"]).default("push"),
|
|
2015
|
+
provisionerUrl: managedEndpointSchema.optional(),
|
|
2016
|
+
settings: z$1.record(z$1.string(), z$1.unknown()),
|
|
2017
|
+
maxTtlSeconds: managedTtlSchema
|
|
2018
|
+
}).strict().superRefine((config, ctx) => {
|
|
2019
|
+
if (!config.provisionerId && config.transport === "push" && !config.provisionerUrl) ctx.addIssue({
|
|
2020
|
+
code: "custom",
|
|
2021
|
+
path: ["provisionerUrl"],
|
|
2022
|
+
message: "HTTPS mode requires a provisioner URL"
|
|
2023
|
+
});
|
|
2024
|
+
if (config.transport === "pull" && config.provisionerUrl) ctx.addIssue({
|
|
2025
|
+
code: "custom",
|
|
2026
|
+
path: ["provisionerUrl"],
|
|
2027
|
+
message: "outbound mode does not use a provisioner URL"
|
|
2028
|
+
});
|
|
2029
|
+
if ([
|
|
2030
|
+
"provider",
|
|
2031
|
+
"executor",
|
|
2032
|
+
"maxTtlSeconds"
|
|
2033
|
+
].some((key) => key in config.settings)) {
|
|
2034
|
+
ctx.addIssue({
|
|
2035
|
+
code: "custom",
|
|
2036
|
+
path: ["settings"],
|
|
2037
|
+
message: "settings contain a reserved field"
|
|
2038
|
+
});
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
if (!config.provisionerId && config.settings.endpoint !== void 0) throw new Error("Register a provisioner to configure its endpoint in Seekrit.");
|
|
2043
|
+
if (config.provisionerId && hasLocalServiceEndpoint(config.provider) && !config.settings.endpoint) throw new Error("Configure the service endpoint in Seekrit.");
|
|
2044
|
+
customerSettingsAsHosted(config.provider, config.settings);
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
if (error instanceof z$1.ZodError) for (const issue of error.issues) ctx.addIssue({
|
|
2047
|
+
code: "custom",
|
|
2048
|
+
path: ["settings", ...issue.path],
|
|
2049
|
+
message: issue.message
|
|
2050
|
+
});
|
|
2051
|
+
else ctx.addIssue({
|
|
2052
|
+
code: "custom",
|
|
2053
|
+
path: ["settings"],
|
|
2054
|
+
message: "invalid service settings"
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}).transform((config) => {
|
|
2058
|
+
const normalized = customerSettingsAsHosted(config.provider, config.settings);
|
|
2059
|
+
return {
|
|
2060
|
+
...config,
|
|
2061
|
+
settings: Object.fromEntries(Object.entries(normalized).filter(([key]) => ![
|
|
2062
|
+
"provider",
|
|
2063
|
+
"executor",
|
|
2064
|
+
"maxTtlSeconds"
|
|
2065
|
+
].includes(key) && (key !== "endpoint" || config.settings.endpoint !== void 0)))
|
|
2066
|
+
};
|
|
2067
|
+
});
|
|
2068
|
+
const managedTargetConfigSchema = z$1.union([hostedManagedTargetConfigSchema, remoteManagedTargetConfigSchema]);
|
|
1972
2069
|
function isManagedLeaseKind(value) {
|
|
1973
2070
|
return MANAGED_LEASE_KINDS.includes(value);
|
|
1974
2071
|
}
|
|
1975
2072
|
function isManagedTarget(value) {
|
|
1976
2073
|
return isManagedLeaseKind(value.provider);
|
|
1977
2074
|
}
|
|
2075
|
+
const id = z$1.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/);
|
|
2076
|
+
const timestamp = z$1.number().int().nonnegative().safe();
|
|
2077
|
+
const provisionerPrincipalSchema = z$1.object({
|
|
2078
|
+
type: z$1.enum(["user", "service_token"]),
|
|
2079
|
+
id,
|
|
2080
|
+
publicKeyJwk: z$1.string().min(1).max(1024)
|
|
2081
|
+
}).strict();
|
|
2082
|
+
const provisionerBootstrapSchema = z$1.object({
|
|
2083
|
+
version: z$1.literal(1),
|
|
2084
|
+
orgId: id,
|
|
2085
|
+
provisionerId: id,
|
|
2086
|
+
authority: provisionerPrincipalSchema,
|
|
2087
|
+
createdAt: timestamp
|
|
2088
|
+
}).strict();
|
|
2089
|
+
const provisionerGrantSchema = z$1.object({
|
|
2090
|
+
version: z$1.literal(1),
|
|
2091
|
+
id,
|
|
2092
|
+
orgId: id,
|
|
2093
|
+
provisionerId: id,
|
|
2094
|
+
principal: provisionerPrincipalSchema,
|
|
2095
|
+
targets: z$1.array(id.or(z$1.literal("*"))).min(1).max(100),
|
|
2096
|
+
manage: z$1.boolean(),
|
|
2097
|
+
issuedAt: timestamp,
|
|
2098
|
+
expiresAt: timestamp
|
|
2099
|
+
}).strict();
|
|
2100
|
+
const provisionerChainSchema = z$1.object({
|
|
2101
|
+
bootstrap: z$1.string().max(4096).startsWith("pb1."),
|
|
2102
|
+
grants: z$1.array(z$1.string().max(4096).startsWith("pg1.")).max(3)
|
|
2103
|
+
}).strict();
|
|
2104
|
+
z$1.object({
|
|
2105
|
+
version: z$1.literal(1),
|
|
2106
|
+
orgId: id,
|
|
2107
|
+
provisionerId: id,
|
|
2108
|
+
targetId: id,
|
|
2109
|
+
provider: id,
|
|
2110
|
+
leaseId: id,
|
|
2111
|
+
instanceId: id,
|
|
2112
|
+
nonce: id,
|
|
2113
|
+
targetDigest: z$1.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
2114
|
+
action: z$1.unknown(),
|
|
2115
|
+
recipientPublicKey: z$1.string().max(1024).optional(),
|
|
2116
|
+
issuedAt: timestamp,
|
|
2117
|
+
expiresAt: timestamp
|
|
2118
|
+
}).strict();
|
|
2119
|
+
const provisionerAuthorizationSchema = provisionerChainSchema.extend({ proof: z$1.string().max(16384).startsWith("pr1.") });
|
|
2120
|
+
const utf8 = new TextEncoder();
|
|
2121
|
+
function b64(bytes) {
|
|
2122
|
+
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
2123
|
+
}
|
|
2124
|
+
function unb64(value) {
|
|
2125
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("Invalid authorization encoding");
|
|
2126
|
+
return Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/")), (c) => c.charCodeAt(0));
|
|
2127
|
+
}
|
|
2128
|
+
function readProvisionerEnvelope(envelope, prefix) {
|
|
2129
|
+
const parts = envelope.split(".");
|
|
2130
|
+
if (envelope.length > 16384 || parts.length !== 3 || parts[0] !== prefix) throw new Error("Invalid customer authorization envelope");
|
|
2131
|
+
return JSON.parse(new TextDecoder("utf-8", {
|
|
2132
|
+
fatal: true,
|
|
2133
|
+
ignoreBOM: false
|
|
2134
|
+
}).decode(unb64(parts[1] ?? "")));
|
|
2135
|
+
}
|
|
2136
|
+
function provisionerPublicKey(value) {
|
|
2137
|
+
const jwk = JSON.parse(value);
|
|
2138
|
+
if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.x || !jwk.y || jwk.d) throw new Error("Customer authorization requires a public P-256 key");
|
|
2139
|
+
return {
|
|
2140
|
+
kty: "EC",
|
|
2141
|
+
crv: "P-256",
|
|
2142
|
+
x: jwk.x,
|
|
2143
|
+
y: jwk.y
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
async function signProvisionerEnvelope(prefix, key, payload) {
|
|
2147
|
+
const encoded = b64(utf8.encode(JSON.stringify(payload)));
|
|
2148
|
+
const signature = await crypto.subtle.sign({
|
|
2149
|
+
name: "ECDSA",
|
|
2150
|
+
hash: "SHA-256"
|
|
2151
|
+
}, key, utf8.encode(`${prefix}\n${encoded}`));
|
|
2152
|
+
return `${prefix}.${encoded}.${b64(new Uint8Array(signature))}`;
|
|
2153
|
+
}
|
|
2154
|
+
async function verifyProvisionerEnvelope(envelope, prefix, publicKeyJwk) {
|
|
2155
|
+
const payload = readProvisionerEnvelope(envelope, prefix);
|
|
2156
|
+
const [, encoded, signature] = envelope.split(".");
|
|
2157
|
+
const key = await crypto.subtle.importKey("jwk", provisionerPublicKey(publicKeyJwk), {
|
|
2158
|
+
name: "ECDSA",
|
|
2159
|
+
namedCurve: "P-256"
|
|
2160
|
+
}, false, ["verify"]);
|
|
2161
|
+
if (!await crypto.subtle.verify({
|
|
2162
|
+
name: "ECDSA",
|
|
2163
|
+
hash: "SHA-256"
|
|
2164
|
+
}, key, unb64(signature ?? ""), utf8.encode(`${prefix}\n${encoded}`))) throw new Error("Invalid customer authorization signature");
|
|
2165
|
+
return payload;
|
|
2166
|
+
}
|
|
2167
|
+
/** The API compares bootstrap to its registration; the Rust service additionally
|
|
2168
|
+
* verifies its MAC with a root-derived key unavailable to the API. */
|
|
2169
|
+
async function verifyProvisionerChain(chain, orgId, provisionerId, now = Date.now()) {
|
|
2170
|
+
provisionerChainSchema.parse(chain);
|
|
2171
|
+
const root = provisionerBootstrapSchema.parse(readProvisionerEnvelope(chain.bootstrap, "pb1"));
|
|
2172
|
+
if (root.orgId !== orgId || root.provisionerId !== provisionerId || root.authority.type !== "user") throw new Error("Customer authorization belongs to another provisioner");
|
|
2173
|
+
provisionerPublicKey(root.authority.publicKeyJwk);
|
|
2174
|
+
let access = {
|
|
2175
|
+
principal: root.authority,
|
|
2176
|
+
targets: ["*"],
|
|
2177
|
+
manage: true,
|
|
2178
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
2179
|
+
chain
|
|
2180
|
+
};
|
|
2181
|
+
for (const envelope of chain.grants) {
|
|
2182
|
+
if (!access.manage) throw new Error("This permission cannot delegate access");
|
|
2183
|
+
const grant = provisionerGrantSchema.parse(await verifyProvisionerEnvelope(envelope, "pg1", access.principal.publicKeyJwk));
|
|
2184
|
+
if (grant.orgId !== orgId || grant.provisionerId !== provisionerId || grant.issuedAt > now + 3e5 || grant.expiresAt <= now || grant.expiresAt <= grant.issuedAt || grant.expiresAt - grant.issuedAt > 7776e6 || grant.expiresAt > access.expiresAt || !grant.targets.every((t) => access.targets.includes("*") || access.targets.includes(t)) || grant.manage && grant.principal.type !== "user") throw new Error("Expired or invalid customer access grant");
|
|
2185
|
+
provisionerPublicKey(grant.principal.publicKeyJwk);
|
|
2186
|
+
access = {
|
|
2187
|
+
principal: grant.principal,
|
|
2188
|
+
targets: grant.targets,
|
|
2189
|
+
manage: grant.manage,
|
|
2190
|
+
expiresAt: grant.expiresAt,
|
|
2191
|
+
chain
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
return access;
|
|
2195
|
+
}
|
|
2196
|
+
/** Targets carry their templates inside encrypted configuration. */
|
|
2197
|
+
function provisionerTransportAction(action) {
|
|
2198
|
+
if (action.kind === "sql") return {
|
|
2199
|
+
kind: "sql",
|
|
2200
|
+
statements: [],
|
|
2201
|
+
parameters: action.parameters
|
|
2202
|
+
};
|
|
2203
|
+
if (action.kind === "ssh-sign") return action;
|
|
2204
|
+
return { kind: "noop" };
|
|
2205
|
+
}
|
|
1978
2206
|
z$1.enum([
|
|
1979
2207
|
"postgres",
|
|
1980
2208
|
"mysql",
|
|
@@ -2052,7 +2280,12 @@ const awsRegionSchema$1 = z$1.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must
|
|
|
2052
2280
|
* documented safe set and bound the length.
|
|
2053
2281
|
*/
|
|
2054
2282
|
const awsExternalIdSchema = z$1.string().regex(/^[\w+=,.@:/-]{2,1224}$/, "must be a valid STS external id");
|
|
2055
|
-
|
|
2283
|
+
/**
|
|
2284
|
+
* An STS session name stamped onto the assumed-role credential (and surfaced in
|
|
2285
|
+
* CloudTrail). We derive it from the lease id, so this is a sanity gate matching
|
|
2286
|
+
* the AWS-documented pattern.
|
|
2287
|
+
*/
|
|
2288
|
+
const awsRoleSessionNameSchema = z$1.string().regex(/^[\w+=,.@-]{2,64}$/, "must be 2–64 chars of [A-Za-z0-9_+=,.@-]");
|
|
2056
2289
|
/**
|
|
2057
2290
|
* A GCP service-account email the broker is allowed to impersonate (or that
|
|
2058
2291
|
* appears in a delegation chain). Structurally validated and bounded: it is
|
|
@@ -2083,7 +2316,14 @@ const p256PublicKeyJwkSchema = z$1.string().max(2048).refine((s) => {
|
|
|
2083
2316
|
return false;
|
|
2084
2317
|
}
|
|
2085
2318
|
}, "must be a JWK-serialized P-256 public key");
|
|
2086
|
-
|
|
2319
|
+
/**
|
|
2320
|
+
* A MongoDB user name. Derived from the lease id (so it's server-chosen, not
|
|
2321
|
+
* user input), but validated as a strict token anyway: MongoDB user documents
|
|
2322
|
+
* are structured BSON — there is no string interpolation to inject into — yet a
|
|
2323
|
+
* bounded `[A-Za-z0-9_.-]` name keeps the created principal readable in
|
|
2324
|
+
* `db.getUsers()` and free of the `.`/`$` characters Mongo reserves.
|
|
2325
|
+
*/
|
|
2326
|
+
const mongoUserNameSchema = z$1.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1–64 chars of [A-Za-z0-9_-]");
|
|
2087
2327
|
/**
|
|
2088
2328
|
* A MongoDB database name — the db a preset role is granted on, or the
|
|
2089
2329
|
* authentication database a leased user is created in. MongoDB forbids
|
|
@@ -2124,6 +2364,11 @@ const mongoAccessLevelSchema = z$1.enum([
|
|
|
2124
2364
|
"readwrite",
|
|
2125
2365
|
"custom"
|
|
2126
2366
|
]);
|
|
2367
|
+
/** The built-in MongoDB role each preset grants on the target database. */
|
|
2368
|
+
const MONGO_PRESET_ROLES = {
|
|
2369
|
+
readonly: "read",
|
|
2370
|
+
readwrite: "readWrite"
|
|
2371
|
+
};
|
|
2127
2372
|
const connectionSchema = z$1.object({
|
|
2128
2373
|
host: z$1.string().min(1),
|
|
2129
2374
|
port: z$1.number().int().min(1).max(65535),
|
|
@@ -2134,6 +2379,8 @@ const statementSchema$1 = z$1.string().min(1).max(4e3);
|
|
|
2134
2379
|
/** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
|
|
2135
2380
|
const identifierSchema = z$1.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
|
|
2136
2381
|
const postgresTargetConfigSchema = z$1.object({
|
|
2382
|
+
tls: z$1.boolean().optional(),
|
|
2383
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2137
2384
|
provider: z$1.literal("postgres"),
|
|
2138
2385
|
executor: executorModeSchema,
|
|
2139
2386
|
accessLevel: postgresAccessLevelSchema.optional(),
|
|
@@ -2146,6 +2393,8 @@ const postgresTargetConfigSchema = z$1.object({
|
|
|
2146
2393
|
/** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
|
|
2147
2394
|
const mysqlHostSchema$1 = z$1.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
|
|
2148
2395
|
const mysqlTargetConfigSchema = z$1.object({
|
|
2396
|
+
tls: z$1.boolean().optional(),
|
|
2397
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2149
2398
|
provider: z$1.literal("mysql"),
|
|
2150
2399
|
executor: executorModeSchema,
|
|
2151
2400
|
accessLevel: mysqlAccessLevelSchema.optional(),
|
|
@@ -2162,6 +2411,7 @@ const redisConnectionSchema = z$1.object({
|
|
|
2162
2411
|
db: z$1.number().int().min(0).max(15).optional()
|
|
2163
2412
|
});
|
|
2164
2413
|
const redisTargetConfigSchema = z$1.object({
|
|
2414
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2165
2415
|
provider: z$1.literal("redis"),
|
|
2166
2416
|
executor: executorModeSchema,
|
|
2167
2417
|
accessLevel: redisAccessLevelSchema.optional(),
|
|
@@ -2171,8 +2421,9 @@ const redisTargetConfigSchema = z$1.object({
|
|
|
2171
2421
|
revokeStatements: z$1.array(statementSchema$1).max(16).optional()
|
|
2172
2422
|
});
|
|
2173
2423
|
const sshTargetConfigSchema = z$1.object({
|
|
2424
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2174
2425
|
provider: z$1.literal("ssh"),
|
|
2175
|
-
executor:
|
|
2426
|
+
executor: executorModeSchema,
|
|
2176
2427
|
caPublicKey: sshPublicKeySchema,
|
|
2177
2428
|
allowedPrincipals: z$1.array(sshPrincipalSchema).max(64).optional(),
|
|
2178
2429
|
extensions: z$1.array(sshExtensionSchema).max(16).optional(),
|
|
@@ -2184,8 +2435,9 @@ const sshTargetConfigSchema = z$1.object({
|
|
|
2184
2435
|
});
|
|
2185
2436
|
const AWS_MAX_TTL_SECONDS = 43200;
|
|
2186
2437
|
const awsTargetConfigSchema = z$1.object({
|
|
2438
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2187
2439
|
provider: z$1.literal("aws"),
|
|
2188
|
-
executor:
|
|
2440
|
+
executor: executorModeSchema,
|
|
2189
2441
|
roleArn: awsRoleArnSchema,
|
|
2190
2442
|
region: awsRegionSchema$1,
|
|
2191
2443
|
externalId: awsExternalIdSchema.optional(),
|
|
@@ -2194,16 +2446,18 @@ const awsTargetConfigSchema = z$1.object({
|
|
|
2194
2446
|
});
|
|
2195
2447
|
const GCP_MAX_TTL_SECONDS = 43200;
|
|
2196
2448
|
const gcpTargetConfigSchema = z$1.object({
|
|
2449
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2197
2450
|
provider: z$1.literal("gcp"),
|
|
2198
|
-
executor:
|
|
2451
|
+
executor: executorModeSchema,
|
|
2199
2452
|
serviceAccount: gcpServiceAccountEmailSchema,
|
|
2200
2453
|
scopes: z$1.array(gcpOauthScopeSchema).min(1).max(32).optional(),
|
|
2201
2454
|
delegates: z$1.array(gcpServiceAccountEmailSchema).max(8).optional(),
|
|
2202
2455
|
maxTtlSeconds: z$1.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
|
|
2203
2456
|
});
|
|
2204
2457
|
const mongoTargetConfigSchema = z$1.object({
|
|
2458
|
+
provisionerId: z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/).optional(),
|
|
2205
2459
|
provider: z$1.literal("mongodb"),
|
|
2206
|
-
executor:
|
|
2460
|
+
executor: executorModeSchema,
|
|
2207
2461
|
accessLevel: mongoAccessLevelSchema.optional(),
|
|
2208
2462
|
connection: connectionSchema,
|
|
2209
2463
|
authSource: mongoDatabaseNameSchema.optional(),
|
|
@@ -2211,7 +2465,7 @@ const mongoTargetConfigSchema = z$1.object({
|
|
|
2211
2465
|
tls: z$1.boolean().optional(),
|
|
2212
2466
|
maxTtlSeconds: z$1.number().int().min(60).max(604800).optional()
|
|
2213
2467
|
});
|
|
2214
|
-
const leaseTargetConfigSchema = z$1.
|
|
2468
|
+
const leaseTargetConfigSchema = z$1.union([
|
|
2215
2469
|
postgresTargetConfigSchema,
|
|
2216
2470
|
mysqlTargetConfigSchema,
|
|
2217
2471
|
redisTargetConfigSchema,
|
|
@@ -2219,9 +2473,26 @@ const leaseTargetConfigSchema = z$1.discriminatedUnion("provider", [
|
|
|
2219
2473
|
awsTargetConfigSchema,
|
|
2220
2474
|
gcpTargetConfigSchema,
|
|
2221
2475
|
mongoTargetConfigSchema,
|
|
2222
|
-
|
|
2223
|
-
])
|
|
2476
|
+
managedTargetConfigSchema
|
|
2477
|
+
]).superRefine((config, ctx) => {
|
|
2478
|
+
if (config.executor === "remote" && [
|
|
2479
|
+
"ssh",
|
|
2480
|
+
"aws",
|
|
2481
|
+
"gcp",
|
|
2482
|
+
"mongodb"
|
|
2483
|
+
].includes(config.provider) && !("provisionerId" in config && config.provisionerId)) ctx.addIssue({
|
|
2484
|
+
code: "custom",
|
|
2485
|
+
path: ["provisionerId"],
|
|
2486
|
+
message: "Select a registered customer provisioner."
|
|
2487
|
+
});
|
|
2488
|
+
if ("provisionerId" in config && config.provisionerId && config.executor !== "remote") ctx.addIssue({
|
|
2489
|
+
code: "custom",
|
|
2490
|
+
path: ["executor"],
|
|
2491
|
+
message: "A provisioner target must run in your network."
|
|
2492
|
+
});
|
|
2493
|
+
});
|
|
2224
2494
|
z$1.object({
|
|
2495
|
+
id: z$1.string().regex(/^lt_[A-Za-z0-9_-]{24}$/).optional(),
|
|
2225
2496
|
name: z$1.string().trim().min(1).max(128),
|
|
2226
2497
|
config: leaseTargetConfigSchema,
|
|
2227
2498
|
/**
|
|
@@ -2230,7 +2501,7 @@ z$1.object({
|
|
|
2230
2501
|
* control plane stores only this ciphertext — it never sees the plaintext.
|
|
2231
2502
|
*/
|
|
2232
2503
|
wrappedAdminSecret: z$1.string().min(1)
|
|
2233
|
-
});
|
|
2504
|
+
}).refine((v) => !("provisionerId" in v.config && v.config.provisionerId) || v.wrappedAdminSecret.startsWith("wd1.") && v.wrappedAdminSecret.length <= 49152, "Encrypted provisioner configuration must be a wd1 blob no larger than 48 KiB");
|
|
2234
2505
|
/** Requested lease lifetime, shared by all providers. */
|
|
2235
2506
|
const ttlSecondsSchema = z$1.number().int().min(60).max(604800);
|
|
2236
2507
|
/**
|
|
@@ -2241,6 +2512,7 @@ const ttlSecondsSchema = z$1.number().int().min(60).max(604800);
|
|
|
2241
2512
|
const mintPostgresLeaseSchema = z$1.object({
|
|
2242
2513
|
provider: z$1.literal("postgres"),
|
|
2243
2514
|
targetId: z$1.string().min(1),
|
|
2515
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2244
2516
|
roleName: postgresRoleNameSchema,
|
|
2245
2517
|
verifier: scramVerifierSchema,
|
|
2246
2518
|
ttlSeconds: ttlSecondsSchema
|
|
@@ -2253,6 +2525,7 @@ const mintPostgresLeaseSchema = z$1.object({
|
|
|
2253
2525
|
const mintMysqlLeaseSchema = z$1.object({
|
|
2254
2526
|
provider: z$1.literal("mysql"),
|
|
2255
2527
|
targetId: z$1.string().min(1),
|
|
2528
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2256
2529
|
roleName: mysqlUserNameSchema,
|
|
2257
2530
|
verifier: mysqlNativeVerifierSchema,
|
|
2258
2531
|
ttlSeconds: ttlSecondsSchema
|
|
@@ -2265,6 +2538,7 @@ const mintMysqlLeaseSchema = z$1.object({
|
|
|
2265
2538
|
const mintRedisLeaseSchema = z$1.object({
|
|
2266
2539
|
provider: z$1.literal("redis"),
|
|
2267
2540
|
targetId: z$1.string().min(1),
|
|
2541
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2268
2542
|
roleName: redisUserNameSchema,
|
|
2269
2543
|
verifier: redisSha256VerifierSchema,
|
|
2270
2544
|
ttlSeconds: ttlSecondsSchema
|
|
@@ -2277,6 +2551,7 @@ const mintRedisLeaseSchema = z$1.object({
|
|
|
2277
2551
|
const mintSshLeaseSchema = z$1.object({
|
|
2278
2552
|
provider: z$1.literal("ssh"),
|
|
2279
2553
|
targetId: z$1.string().min(1),
|
|
2554
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2280
2555
|
publicKey: sshPublicKeySchema,
|
|
2281
2556
|
principals: z$1.array(sshPrincipalSchema).min(1).max(32),
|
|
2282
2557
|
ttlSeconds: ttlSecondsSchema
|
|
@@ -2293,6 +2568,7 @@ const mintSshLeaseSchema = z$1.object({
|
|
|
2293
2568
|
const mintAwsLeaseSchema = z$1.object({
|
|
2294
2569
|
provider: z$1.literal("aws"),
|
|
2295
2570
|
targetId: z$1.string().min(1),
|
|
2571
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2296
2572
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
2297
2573
|
ttlSeconds: z$1.number().int().min(900).max(AWS_MAX_TTL_SECONDS)
|
|
2298
2574
|
});
|
|
@@ -2309,6 +2585,7 @@ const mintAwsLeaseSchema = z$1.object({
|
|
|
2309
2585
|
const mintGcpLeaseSchema = z$1.object({
|
|
2310
2586
|
provider: z$1.literal("gcp"),
|
|
2311
2587
|
targetId: z$1.string().min(1),
|
|
2588
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2312
2589
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
2313
2590
|
ttlSeconds: z$1.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
|
|
2314
2591
|
});
|
|
@@ -2322,12 +2599,14 @@ const mintGcpLeaseSchema = z$1.object({
|
|
|
2322
2599
|
const mintMongoLeaseSchema = z$1.object({
|
|
2323
2600
|
provider: z$1.literal("mongodb"),
|
|
2324
2601
|
targetId: z$1.string().min(1),
|
|
2602
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2325
2603
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
2326
2604
|
ttlSeconds: ttlSecondsSchema
|
|
2327
2605
|
});
|
|
2328
2606
|
const mintManagedLeaseSchema = z$1.object({
|
|
2329
2607
|
provider: z$1.enum(MANAGED_LEASE_KINDS),
|
|
2330
2608
|
targetId: z$1.string().min(1),
|
|
2609
|
+
authorization: provisionerAuthorizationSchema.optional(),
|
|
2331
2610
|
recipientPublicKey: p256PublicKeyJwkSchema.refine((value) => {
|
|
2332
2611
|
try {
|
|
2333
2612
|
return !("d" in JSON.parse(value));
|
|
@@ -2347,6 +2626,57 @@ z$1.discriminatedUnion("provider", [
|
|
|
2347
2626
|
mintMongoLeaseSchema,
|
|
2348
2627
|
mintManagedLeaseSchema
|
|
2349
2628
|
]);
|
|
2629
|
+
// ../../packages/core/src/providers/aws.ts
|
|
2630
|
+
/**
|
|
2631
|
+
* Tier-2 AWS provider. Unlike the verifier-injection providers, the target
|
|
2632
|
+
* (AWS/STS) mints the credential itself, so `render` cannot produce a
|
|
2633
|
+
* zero-knowledge verifier — instead it produces an {@link AwsStsSpec} the broker
|
|
2634
|
+
* executor runs: assume `roleArn` via STS, then wrap the returned credential to
|
|
2635
|
+
* the consumer's ephemeral public key before returning it. `render` stays pure
|
|
2636
|
+
* and secret-free (it never touches the admin credential or the network); the
|
|
2637
|
+
* consumer's private key never leaves their machine, so the plaintext
|
|
2638
|
+
* credential is only ever decryptable there.
|
|
2639
|
+
*
|
|
2640
|
+
* Revocation is a no-op: STS session credentials can't be revoked individually,
|
|
2641
|
+
* so v1 relies on the short `DurationSeconds` STS itself enforces — exactly like
|
|
2642
|
+
* the SSH provider's short-TTL certificates.
|
|
2643
|
+
*/
|
|
2644
|
+
/** Clamp a requested TTL into STS's `DurationSeconds` window and the target's cap. */
|
|
2645
|
+
function clampDuration(requested, maxTtlSeconds) {
|
|
2646
|
+
return Math.max(900, Math.min(requested, Math.min(maxTtlSeconds ?? 43200, AWS_MAX_TTL_SECONDS)));
|
|
2647
|
+
}
|
|
2648
|
+
const awsProvider = {
|
|
2649
|
+
kind: "aws",
|
|
2650
|
+
zkTier: 2,
|
|
2651
|
+
render(config, req, ctx) {
|
|
2652
|
+
if (req.provider !== "aws") throw new Error("aws provider received a non-aws request");
|
|
2653
|
+
const cfg = config;
|
|
2654
|
+
awsRoleArnSchema.parse(cfg.roleArn);
|
|
2655
|
+
p256PublicKeyJwkSchema.parse(req.recipientPublicKey);
|
|
2656
|
+
const roleSessionName = awsRoleSessionNameSchema.parse(ctx.leaseId);
|
|
2657
|
+
const durationSeconds = clampDuration(req.ttlSeconds, cfg.maxTtlSeconds);
|
|
2658
|
+
return {
|
|
2659
|
+
resourceRef: roleSessionName,
|
|
2660
|
+
expiresAt: new Date(ctx.nowMs + durationSeconds * 1e3).toISOString(),
|
|
2661
|
+
provision: {
|
|
2662
|
+
kind: "aws-sts",
|
|
2663
|
+
spec: {
|
|
2664
|
+
roleArn: cfg.roleArn,
|
|
2665
|
+
roleSessionName,
|
|
2666
|
+
durationSeconds,
|
|
2667
|
+
region: cfg.region,
|
|
2668
|
+
externalId: cfg.externalId,
|
|
2669
|
+
policy: cfg.sessionPolicy,
|
|
2670
|
+
recipientPublicKey: req.recipientPublicKey
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2673
|
+
revoke: { kind: "noop" }
|
|
2674
|
+
};
|
|
2675
|
+
},
|
|
2676
|
+
renderRevoke() {
|
|
2677
|
+
return { kind: "noop" };
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2350
2680
|
/**
|
|
2351
2681
|
* The one-time IAM setup an admin performs so seekrit can assume the target
|
|
2352
2682
|
* role. Analogue of `sshHostSetupInstructions` / `postgresGroupBootstrapSql` —
|
|
@@ -2373,6 +2703,59 @@ function awsTrustPolicyInstructions(config) {
|
|
|
2373
2703
|
`# on ${config.roleArn}. seekrit only ever calls AssumeRole on this role.`
|
|
2374
2704
|
].join("\n");
|
|
2375
2705
|
}
|
|
2706
|
+
// ../../packages/core/src/providers/gcp.ts
|
|
2707
|
+
/**
|
|
2708
|
+
* Tier-2 GCP provider. Like {@link awsProvider}, the target (GCP's IAM
|
|
2709
|
+
* Credentials API) mints the credential itself, so `render` cannot produce a
|
|
2710
|
+
* zero-knowledge verifier — instead it produces a {@link GcpImpersonateSpec} the
|
|
2711
|
+
* broker executor runs: impersonate `serviceAccount` via `generateAccessToken`,
|
|
2712
|
+
* then wrap the returned OAuth token to the consumer's ephemeral public key
|
|
2713
|
+
* before returning it. `render` stays pure and secret-free (it never touches the
|
|
2714
|
+
* admin key or the network); the consumer's private key never leaves their
|
|
2715
|
+
* machine, so the plaintext token is only ever decryptable there.
|
|
2716
|
+
*
|
|
2717
|
+
* Revocation is a no-op: short-lived access tokens can't be revoked
|
|
2718
|
+
* individually, so v1 relies on the short `lifetime` GCP itself enforces —
|
|
2719
|
+
* exactly like the AWS and SSH providers.
|
|
2720
|
+
*/
|
|
2721
|
+
/** The default scope when a target sets none — full API access, IAM-scoped. */
|
|
2722
|
+
const GCP_DEFAULT_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
|
|
2723
|
+
/** Clamp a requested TTL into GCP's `lifetime` window and the target's cap. */
|
|
2724
|
+
function clampLifetime(requested, maxTtlSeconds) {
|
|
2725
|
+
return Math.max(60, Math.min(requested, Math.min(maxTtlSeconds ?? 43200, GCP_MAX_TTL_SECONDS)));
|
|
2726
|
+
}
|
|
2727
|
+
const gcpProvider = {
|
|
2728
|
+
kind: "gcp",
|
|
2729
|
+
zkTier: 2,
|
|
2730
|
+
render(config, req, ctx) {
|
|
2731
|
+
if (req.provider !== "gcp") throw new Error("gcp provider received a non-gcp request");
|
|
2732
|
+
const cfg = config;
|
|
2733
|
+
gcpServiceAccountEmailSchema.parse(cfg.serviceAccount);
|
|
2734
|
+
p256PublicKeyJwkSchema.parse(req.recipientPublicKey);
|
|
2735
|
+
const scopes = cfg.scopes?.length ? cfg.scopes : [GCP_DEFAULT_SCOPE];
|
|
2736
|
+
for (const scope of scopes) gcpOauthScopeSchema.parse(scope);
|
|
2737
|
+
const lifetimeSeconds = clampLifetime(req.ttlSeconds, cfg.maxTtlSeconds);
|
|
2738
|
+
const expiresAt = new Date(ctx.nowMs + lifetimeSeconds * 1e3).toISOString();
|
|
2739
|
+
return {
|
|
2740
|
+
resourceRef: cfg.serviceAccount,
|
|
2741
|
+
expiresAt,
|
|
2742
|
+
provision: {
|
|
2743
|
+
kind: "gcp-iam",
|
|
2744
|
+
spec: {
|
|
2745
|
+
serviceAccount: cfg.serviceAccount,
|
|
2746
|
+
scopes,
|
|
2747
|
+
lifetimeSeconds,
|
|
2748
|
+
delegates: cfg.delegates,
|
|
2749
|
+
recipientPublicKey: req.recipientPublicKey
|
|
2750
|
+
}
|
|
2751
|
+
},
|
|
2752
|
+
revoke: { kind: "noop" }
|
|
2753
|
+
};
|
|
2754
|
+
},
|
|
2755
|
+
renderRevoke() {
|
|
2756
|
+
return { kind: "noop" };
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2376
2759
|
/**
|
|
2377
2760
|
* The one-time IAM setup an admin performs so seekrit can impersonate the target
|
|
2378
2761
|
* service account. Analogue of `awsTrustPolicyInstructions` — printed for the
|
|
@@ -2394,6 +2777,145 @@ function gcpSetupInstructions(config) {
|
|
|
2394
2777
|
"gcloud services enable iamcredentials.googleapis.com"
|
|
2395
2778
|
].join("\n");
|
|
2396
2779
|
}
|
|
2780
|
+
// ../../packages/core/src/providers/managed.ts
|
|
2781
|
+
function managedLeaseProvider(kind) {
|
|
2782
|
+
return {
|
|
2783
|
+
kind,
|
|
2784
|
+
zkTier: 2,
|
|
2785
|
+
render(target, request, context) {
|
|
2786
|
+
const config = managedTargetConfigSchema.parse(target);
|
|
2787
|
+
if (config.provider !== kind || request.provider !== kind || !("recipientPublicKey" in request)) throw new Error("managed credential provider mismatch");
|
|
2788
|
+
p256PublicKeyJwkSchema.parse(request.recipientPublicKey);
|
|
2789
|
+
const name = `sk_${context.leaseId}`;
|
|
2790
|
+
if (!/^sk_[a-zA-Z0-9_-]{1,64}$/.test(name)) throw new Error("invalid lease id");
|
|
2791
|
+
const ttl = Math.min(request.ttlSeconds, config.maxTtlSeconds ?? 604800);
|
|
2792
|
+
const expiresAt = new Date(context.nowMs + ttl * 1e3).toISOString();
|
|
2793
|
+
const spec = {
|
|
2794
|
+
config,
|
|
2795
|
+
name,
|
|
2796
|
+
expiresAt,
|
|
2797
|
+
recipientPublicKey: request.recipientPublicKey
|
|
2798
|
+
};
|
|
2799
|
+
return {
|
|
2800
|
+
resourceRef: name,
|
|
2801
|
+
expiresAt,
|
|
2802
|
+
provision: {
|
|
2803
|
+
kind: "managed-credential",
|
|
2804
|
+
spec
|
|
2805
|
+
},
|
|
2806
|
+
revoke: {
|
|
2807
|
+
kind: "managed-credential",
|
|
2808
|
+
spec: {
|
|
2809
|
+
...spec,
|
|
2810
|
+
recipientPublicKey: ""
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
},
|
|
2815
|
+
renderRevoke(target, resourceRef) {
|
|
2816
|
+
const config = managedTargetConfigSchema.parse(target);
|
|
2817
|
+
if (config.provider !== kind || !/^sk_[a-zA-Z0-9_-]{1,64}$/.test(resourceRef)) throw new Error("invalid managed credential revoke");
|
|
2818
|
+
return {
|
|
2819
|
+
kind: "managed-credential",
|
|
2820
|
+
spec: {
|
|
2821
|
+
config,
|
|
2822
|
+
name: resourceRef,
|
|
2823
|
+
recipientPublicKey: "",
|
|
2824
|
+
expiresAt: ""
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
// ../../packages/core/src/providers/mongodb.ts
|
|
2831
|
+
/**
|
|
2832
|
+
* Tier-2 MongoDB provider. Unlike the verifier-injection providers, MongoDB's
|
|
2833
|
+
* `createUser` hashes the password *server-side* (there is no supported path to
|
|
2834
|
+
* inject a precomputed SCRAM credential), so `render` cannot produce a
|
|
2835
|
+
* zero-knowledge verifier. Instead it produces a {@link MongoUserSpec} the
|
|
2836
|
+
* broker executor runs: generate a password, `createUser` with the preset
|
|
2837
|
+
* roles, then wrap the credential to the consumer's ephemeral public key before
|
|
2838
|
+
* returning it. `render` stays pure and secret-free — it never generates the
|
|
2839
|
+
* password, touches the admin credential, or hits the network; the consumer's
|
|
2840
|
+
* private key never leaves their machine, so the plaintext credential is only
|
|
2841
|
+
* ever decryptable there.
|
|
2842
|
+
*
|
|
2843
|
+
* Revocation is real (unlike AWS/SSH): the broker's alarm-driven `dropUser` ends
|
|
2844
|
+
* the lease. Access levels map to MongoDB's built-in roles — `read` /
|
|
2845
|
+
* `readWrite` on the target database — or an explicit `roles` array for custom.
|
|
2846
|
+
*/
|
|
2847
|
+
/** Clamp a requested TTL to the target's optional cap. */
|
|
2848
|
+
function clampTtl(requested, maxTtlSeconds) {
|
|
2849
|
+
return maxTtlSeconds ? Math.min(requested, maxTtlSeconds) : requested;
|
|
2850
|
+
}
|
|
2851
|
+
/** The role grants a target's access level maps to. Defaults to read-only. */
|
|
2852
|
+
function rolesFor(config) {
|
|
2853
|
+
const db = config.connection.database;
|
|
2854
|
+
if (config.accessLevel === "custom") {
|
|
2855
|
+
if (!config.roles || config.roles.length === 0) throw new Error("mongodb custom target requires a non-empty roles array");
|
|
2856
|
+
return config.roles;
|
|
2857
|
+
}
|
|
2858
|
+
const preset = config.accessLevel === "readwrite" ? "readwrite" : "readonly";
|
|
2859
|
+
return [{
|
|
2860
|
+
role: MONGO_PRESET_ROLES[preset],
|
|
2861
|
+
db
|
|
2862
|
+
}];
|
|
2863
|
+
}
|
|
2864
|
+
const mongoProvider = {
|
|
2865
|
+
kind: "mongodb",
|
|
2866
|
+
zkTier: 2,
|
|
2867
|
+
render(config, req, ctx) {
|
|
2868
|
+
if (config.provider !== "mongodb") throw new Error("mongoProvider given a non-mongodb config");
|
|
2869
|
+
if (req.provider !== "mongodb") throw new Error("mongo provider received a non-mongodb request");
|
|
2870
|
+
p256PublicKeyJwkSchema.parse(req.recipientPublicKey);
|
|
2871
|
+
const username = mongoUserNameSchema.parse(ctx.leaseId);
|
|
2872
|
+
const authSource = mongoDatabaseNameSchema.parse(config.authSource ?? "admin");
|
|
2873
|
+
const database = mongoDatabaseNameSchema.parse(config.connection.database);
|
|
2874
|
+
const roles = rolesFor(config);
|
|
2875
|
+
const ttlSeconds = clampTtl(req.ttlSeconds, config.maxTtlSeconds);
|
|
2876
|
+
const expiresAt = new Date(ctx.nowMs + ttlSeconds * 1e3).toISOString();
|
|
2877
|
+
return {
|
|
2878
|
+
resourceRef: username,
|
|
2879
|
+
expiresAt,
|
|
2880
|
+
provision: {
|
|
2881
|
+
kind: "mongo-user",
|
|
2882
|
+
spec: {
|
|
2883
|
+
username,
|
|
2884
|
+
authSource,
|
|
2885
|
+
database,
|
|
2886
|
+
roles,
|
|
2887
|
+
recipientPublicKey: req.recipientPublicKey,
|
|
2888
|
+
expiresAt
|
|
2889
|
+
}
|
|
2890
|
+
},
|
|
2891
|
+
revoke: {
|
|
2892
|
+
kind: "mongo-user",
|
|
2893
|
+
spec: {
|
|
2894
|
+
username,
|
|
2895
|
+
authSource,
|
|
2896
|
+
database: "",
|
|
2897
|
+
roles: [],
|
|
2898
|
+
recipientPublicKey: "",
|
|
2899
|
+
expiresAt: ""
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
},
|
|
2904
|
+
renderRevoke(config, resourceRef) {
|
|
2905
|
+
if (config.provider !== "mongodb") throw new Error("mongoProvider given a non-mongodb config");
|
|
2906
|
+
return {
|
|
2907
|
+
kind: "mongo-user",
|
|
2908
|
+
spec: {
|
|
2909
|
+
username: mongoUserNameSchema.parse(resourceRef),
|
|
2910
|
+
authSource: mongoDatabaseNameSchema.parse(config.authSource ?? "admin"),
|
|
2911
|
+
database: "",
|
|
2912
|
+
roles: [],
|
|
2913
|
+
recipientPublicKey: "",
|
|
2914
|
+
expiresAt: ""
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2397
2919
|
/**
|
|
2398
2920
|
* The one-time setup an admin performs so seekrit can provision users: create a
|
|
2399
2921
|
* dedicated provisioning user with `userAdmin` on the target database (or
|
|
@@ -2418,6 +2940,187 @@ function mongoAdminSetupInstructions(config) {
|
|
|
2418
2940
|
`// --uri "mongodb://seekrit_provisioner:<pwd>@${config.connection.host}:${config.connection.port}/?authSource=${authSource}"`
|
|
2419
2941
|
].join("\n");
|
|
2420
2942
|
}
|
|
2943
|
+
// ../../packages/core/src/providers/mysql.ts
|
|
2944
|
+
/**
|
|
2945
|
+
* Tier-1 MySQL/MariaDB provider. The consumer generated the password and its
|
|
2946
|
+
* `mysql_native_password` verifier locally (see @seekrit/crypto
|
|
2947
|
+
* `mysqlNativePasswordVerifier`); we only ever see the verifier, which
|
|
2948
|
+
* `CREATE USER … IDENTIFIED WITH mysql_native_password AS '<hash>'` stores
|
|
2949
|
+
* verbatim and which cannot be used to authenticate (login is a
|
|
2950
|
+
* challenge-response needing a `SHA1(password)` preimage). So the handed-out
|
|
2951
|
+
* credential is zero-knowledge end to end — the provider merely renders SQL.
|
|
2952
|
+
*
|
|
2953
|
+
* Unlike Postgres there is no account-level `VALID UNTIL`: the broker's
|
|
2954
|
+
* alarm-driven `DROP USER` is the sole expiry mechanism. Preset access levels
|
|
2955
|
+
* apply inline `GRANT`s (MySQL roles need activation, an awkward fit for a
|
|
2956
|
+
* plain connection URL), so there is no shared group role / setup SQL.
|
|
2957
|
+
*/
|
|
2958
|
+
const DEFAULT_HOST = "%";
|
|
2959
|
+
/** Mirror of the registration-time host schema (render must not trust inputs). */
|
|
2960
|
+
const HOST_RE = /^[A-Za-z0-9_.%:-]{1,255}$/;
|
|
2961
|
+
/** Escape a backtick-quoted identifier by doubling backticks. */
|
|
2962
|
+
function ident(name) {
|
|
2963
|
+
return name.replace(/`/g, "``");
|
|
2964
|
+
}
|
|
2965
|
+
function render$2(template, vars) {
|
|
2966
|
+
return template.replace(/\{\{(name|verifier)\}\}/g, (_, key) => key === "name" ? vars.name : vars.verifier);
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Concrete create/revoke templates for a target. Presets synthesize a
|
|
2970
|
+
* `CREATE USER` plus an inline `GRANT` scoped to the target database; custom
|
|
2971
|
+
* (or a legacy target with no access level) uses the admin's statements,
|
|
2972
|
+
* falling back to a bare CREATE USER / DROP USER. Host and database come from
|
|
2973
|
+
* the (trusted, registration-validated) config and are baked in directly;
|
|
2974
|
+
* only `{{name}}`/`{{verifier}}` remain as per-lease placeholders.
|
|
2975
|
+
*/
|
|
2976
|
+
function statementsFor$2(config) {
|
|
2977
|
+
const account = `'{{name}}'@'${config.userHost && HOST_RE.test(config.userHost) ? config.userHost : DEFAULT_HOST}'`;
|
|
2978
|
+
const create = `CREATE USER ${account} IDENTIFIED WITH mysql_native_password AS '{{verifier}}';`;
|
|
2979
|
+
const revoke = `DROP USER IF EXISTS ${account};`;
|
|
2980
|
+
if (config.accessLevel === "readonly" || config.accessLevel === "readwrite") {
|
|
2981
|
+
const db = ident(config.connection.database);
|
|
2982
|
+
return {
|
|
2983
|
+
create: [create, `GRANT ${config.accessLevel === "readonly" ? "SELECT" : "SELECT, INSERT, UPDATE, DELETE"} ON \`${db}\`.* TO ${account};`],
|
|
2984
|
+
revoke: [revoke]
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
return {
|
|
2988
|
+
create: config.createStatements ?? [create],
|
|
2989
|
+
revoke: config.revokeStatements ?? [revoke]
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
const mysqlProvider = {
|
|
2993
|
+
kind: "mysql",
|
|
2994
|
+
zkTier: 1,
|
|
2995
|
+
render(config, req, ctx) {
|
|
2996
|
+
if (config.provider !== "mysql") throw new Error("mysqlProvider given a non-mysql config");
|
|
2997
|
+
if (req.provider !== "mysql") throw new Error("mysql provider received a non-mysql request");
|
|
2998
|
+
mysqlUserNameSchema.parse(req.roleName);
|
|
2999
|
+
mysqlNativeVerifierSchema.parse(req.verifier);
|
|
3000
|
+
const expiresAt = new Date(ctx.nowMs + req.ttlSeconds * 1e3).toISOString();
|
|
3001
|
+
const vars = {
|
|
3002
|
+
name: req.roleName,
|
|
3003
|
+
verifier: req.verifier
|
|
3004
|
+
};
|
|
3005
|
+
const { create, revoke } = statementsFor$2(config);
|
|
3006
|
+
const parameters = config.executor === "remote" && config.provisionerId ? { parameters: {
|
|
3007
|
+
name: req.roleName,
|
|
3008
|
+
verifier: req.verifier,
|
|
3009
|
+
validUntil: expiresAt
|
|
3010
|
+
} } : {};
|
|
3011
|
+
return {
|
|
3012
|
+
resourceRef: req.roleName,
|
|
3013
|
+
expiresAt,
|
|
3014
|
+
provision: {
|
|
3015
|
+
kind: "sql",
|
|
3016
|
+
...parameters,
|
|
3017
|
+
statements: create.map((s) => render$2(s, vars))
|
|
3018
|
+
},
|
|
3019
|
+
revoke: {
|
|
3020
|
+
kind: "sql",
|
|
3021
|
+
...parameters,
|
|
3022
|
+
statements: revoke.map((s) => render$2(s, {
|
|
3023
|
+
name: req.roleName,
|
|
3024
|
+
verifier: ""
|
|
3025
|
+
}))
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
},
|
|
3029
|
+
renderRevoke(config, resourceRef) {
|
|
3030
|
+
if (config.provider !== "mysql") throw new Error("mysqlProvider given a non-mysql config");
|
|
3031
|
+
mysqlUserNameSchema.parse(resourceRef);
|
|
3032
|
+
return {
|
|
3033
|
+
kind: "sql",
|
|
3034
|
+
statements: statementsFor$2(config).revoke.map((s) => render$2(s, {
|
|
3035
|
+
name: resourceRef,
|
|
3036
|
+
verifier: ""
|
|
3037
|
+
}))
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
// ../../packages/core/src/providers/postgres.ts
|
|
3042
|
+
/**
|
|
3043
|
+
* Tier-1 Postgres provider. The consumer generated the password and its SCRAM
|
|
3044
|
+
* verifier locally (see @seekrit/crypto `scramSha256Verifier`); we only ever
|
|
3045
|
+
* see the verifier, which `CREATE ROLE … PASSWORD` stores verbatim and which
|
|
3046
|
+
* cannot be used to authenticate. So the handed-out credential is
|
|
3047
|
+
* zero-knowledge end to end — the provider merely renders SQL.
|
|
3048
|
+
*
|
|
3049
|
+
* Per-lease inputs are strict tokens (validated role name + verifier) and the
|
|
3050
|
+
* SQL is either an admin-configured template (custom) or derived from a preset
|
|
3051
|
+
* access level backed by a shared group role, so substitution cannot inject SQL.
|
|
3052
|
+
*/
|
|
3053
|
+
const DEFAULT_CREATE$1 = [`CREATE ROLE "{{name}}" LOGIN PASSWORD '{{verifier}}' VALID UNTIL '{{valid_until}}';`];
|
|
3054
|
+
const DEFAULT_REVOKE$1 = [`DROP ROLE IF EXISTS "{{name}}";`];
|
|
3055
|
+
function render$1(template, vars) {
|
|
3056
|
+
return template.replace(/\{\{(name|verifier|valid_until)\}\}/g, (_, key) => vars[key] ?? "");
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* The concrete create/revoke templates for a target. Read-only and read-write
|
|
3060
|
+
* create the temp role `IN ROLE <group>` so it inherits the group's grants;
|
|
3061
|
+
* custom (or a legacy target with no access level) uses the admin's statements,
|
|
3062
|
+
* falling back to a bare CREATE ROLE.
|
|
3063
|
+
*/
|
|
3064
|
+
function statementsFor$1(config) {
|
|
3065
|
+
if (config.accessLevel === "readonly" || config.accessLevel === "readwrite") return {
|
|
3066
|
+
create: [`CREATE ROLE "{{name}}" LOGIN PASSWORD '{{verifier}}' VALID UNTIL '{{valid_until}}' IN ROLE ${POSTGRES_GROUP_ROLES[config.accessLevel]};`],
|
|
3067
|
+
revoke: DEFAULT_REVOKE$1
|
|
3068
|
+
};
|
|
3069
|
+
return {
|
|
3070
|
+
create: config.createStatements ?? DEFAULT_CREATE$1,
|
|
3071
|
+
revoke: config.revokeStatements ?? DEFAULT_REVOKE$1
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
const postgresProvider = {
|
|
3075
|
+
kind: "postgres",
|
|
3076
|
+
zkTier: 1,
|
|
3077
|
+
render(config, req, ctx) {
|
|
3078
|
+
if (req.provider !== "postgres") throw new Error("postgres provider received a non-postgres request");
|
|
3079
|
+
postgresRoleNameSchema.parse(req.roleName);
|
|
3080
|
+
scramVerifierSchema.parse(req.verifier);
|
|
3081
|
+
const expiresAt = new Date(ctx.nowMs + req.ttlSeconds * 1e3).toISOString();
|
|
3082
|
+
const vars = {
|
|
3083
|
+
name: req.roleName,
|
|
3084
|
+
verifier: req.verifier,
|
|
3085
|
+
valid_until: expiresAt
|
|
3086
|
+
};
|
|
3087
|
+
const { create, revoke } = statementsFor$1(config);
|
|
3088
|
+
const parameters = config.executor === "remote" && config.provisionerId ? { parameters: {
|
|
3089
|
+
name: req.roleName,
|
|
3090
|
+
verifier: req.verifier,
|
|
3091
|
+
validUntil: expiresAt
|
|
3092
|
+
} } : {};
|
|
3093
|
+
return {
|
|
3094
|
+
resourceRef: req.roleName,
|
|
3095
|
+
expiresAt,
|
|
3096
|
+
provision: {
|
|
3097
|
+
kind: "sql",
|
|
3098
|
+
...parameters,
|
|
3099
|
+
statements: create.map((s) => render$1(s, vars))
|
|
3100
|
+
},
|
|
3101
|
+
revoke: {
|
|
3102
|
+
kind: "sql",
|
|
3103
|
+
...parameters,
|
|
3104
|
+
statements: revoke.map((s) => render$1(s, {
|
|
3105
|
+
name: req.roleName,
|
|
3106
|
+
verifier: "",
|
|
3107
|
+
valid_until: ""
|
|
3108
|
+
}))
|
|
3109
|
+
}
|
|
3110
|
+
};
|
|
3111
|
+
},
|
|
3112
|
+
renderRevoke(config, resourceRef) {
|
|
3113
|
+
postgresRoleNameSchema.parse(resourceRef);
|
|
3114
|
+
return {
|
|
3115
|
+
kind: "sql",
|
|
3116
|
+
statements: statementsFor$1(config).revoke.map((s) => render$1(s, {
|
|
3117
|
+
name: resourceRef,
|
|
3118
|
+
verifier: "",
|
|
3119
|
+
valid_until: ""
|
|
3120
|
+
}))
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
2421
3124
|
/**
|
|
2422
3125
|
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
2423
3126
|
* read-only / read-write target's leased credentials inherit. Idempotent (safe
|
|
@@ -2448,6 +3151,156 @@ function postgresGroupBootstrapSql(config) {
|
|
|
2448
3151
|
if (config.accessLevel === "readwrite") lines.push(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO ${group};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT USAGE, SELECT ON SEQUENCES TO ${group};`);
|
|
2449
3152
|
return lines.join("\n");
|
|
2450
3153
|
}
|
|
3154
|
+
// ../../packages/core/src/providers/redis.ts
|
|
3155
|
+
/**
|
|
3156
|
+
* Tier-1 Redis (6+) provider. The consumer generated the password and its
|
|
3157
|
+
* SHA-256 hex digest locally (see @seekrit/crypto `redisSha256Verifier`); we
|
|
3158
|
+
* only ever see the digest, which `ACL SETUSER … on #<hex>` stores verbatim and
|
|
3159
|
+
* which cannot be used to authenticate (Redis `AUTH` hashes the plaintext it
|
|
3160
|
+
* receives and compares, so the stored digest is a preimage). So the handed-out
|
|
3161
|
+
* credential is zero-knowledge end to end — the provider merely renders
|
|
3162
|
+
* commands.
|
|
3163
|
+
*
|
|
3164
|
+
* Unlike Postgres there is no account-level expiry: the broker's alarm-driven
|
|
3165
|
+
* `ACL DELUSER` is the sole expiry mechanism (as with MySQL). Preset access
|
|
3166
|
+
* levels apply inline ACL rules (Redis ACL has no role inheritance), so there is
|
|
3167
|
+
* no shared group role / setup step.
|
|
3168
|
+
*
|
|
3169
|
+
* Each rendered statement is a single Redis command line; the executor
|
|
3170
|
+
* whitespace-tokenizes it into RESP arguments. The per-lease inputs are strict
|
|
3171
|
+
* tokens (validated user name + hex digest) with no whitespace or ACL-rule
|
|
3172
|
+
* characters, so substitution cannot inject an extra argument or permission.
|
|
3173
|
+
*/
|
|
3174
|
+
const DEFAULT_CREATE = ["ACL SETUSER {{name}} on #{{verifier}}"];
|
|
3175
|
+
const DEFAULT_REVOKE = ["ACL DELUSER {{name}}"];
|
|
3176
|
+
/**
|
|
3177
|
+
* Inline ACL rules each preset grants: all keys (`~*`), the read (or read+write)
|
|
3178
|
+
* command category, and `+@connection` so client-handshake commands (HELLO,
|
|
3179
|
+
* PING, AUTH) work. No pub/sub channels (`&*`) — the categories don't enable
|
|
3180
|
+
* pub/sub anyway, and `&*` needs Redis 6.2+.
|
|
3181
|
+
*/
|
|
3182
|
+
const PRESET_RULES = {
|
|
3183
|
+
readonly: "~* +@read +@connection",
|
|
3184
|
+
readwrite: "~* +@read +@write +@connection"
|
|
3185
|
+
};
|
|
3186
|
+
function render(template, vars) {
|
|
3187
|
+
return template.replace(/\{\{(name|verifier)\}\}/g, (_, key) => key === "name" ? vars.name : vars.verifier);
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Concrete create/revoke command lines for a target. Presets synthesize an
|
|
3191
|
+
* `ACL SETUSER` that enables the user (`on`), sets its password digest, and
|
|
3192
|
+
* grants the preset's inline rules; custom (or a legacy target with no access
|
|
3193
|
+
* level) uses the admin's statements, falling back to a no-privilege user.
|
|
3194
|
+
*/
|
|
3195
|
+
function statementsFor(config) {
|
|
3196
|
+
if (config.accessLevel === "readonly" || config.accessLevel === "readwrite") return {
|
|
3197
|
+
create: [`ACL SETUSER {{name}} on #{{verifier}} ${PRESET_RULES[config.accessLevel]}`],
|
|
3198
|
+
revoke: DEFAULT_REVOKE
|
|
3199
|
+
};
|
|
3200
|
+
return {
|
|
3201
|
+
create: config.createStatements ?? DEFAULT_CREATE,
|
|
3202
|
+
revoke: config.revokeStatements ?? DEFAULT_REVOKE
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
const redisProvider = {
|
|
3206
|
+
kind: "redis",
|
|
3207
|
+
zkTier: 1,
|
|
3208
|
+
render(config, req, ctx) {
|
|
3209
|
+
if (config.provider !== "redis") throw new Error("redisProvider given a non-redis config");
|
|
3210
|
+
if (req.provider !== "redis") throw new Error("redis provider received a non-redis request");
|
|
3211
|
+
redisUserNameSchema.parse(req.roleName);
|
|
3212
|
+
redisSha256VerifierSchema.parse(req.verifier);
|
|
3213
|
+
const expiresAt = new Date(ctx.nowMs + req.ttlSeconds * 1e3).toISOString();
|
|
3214
|
+
const vars = {
|
|
3215
|
+
name: req.roleName,
|
|
3216
|
+
verifier: req.verifier
|
|
3217
|
+
};
|
|
3218
|
+
const { create, revoke } = statementsFor(config);
|
|
3219
|
+
const parameters = config.executor === "remote" && config.provisionerId ? { parameters: {
|
|
3220
|
+
name: req.roleName,
|
|
3221
|
+
verifier: req.verifier,
|
|
3222
|
+
validUntil: expiresAt
|
|
3223
|
+
} } : {};
|
|
3224
|
+
return {
|
|
3225
|
+
resourceRef: req.roleName,
|
|
3226
|
+
expiresAt,
|
|
3227
|
+
provision: {
|
|
3228
|
+
kind: "sql",
|
|
3229
|
+
...parameters,
|
|
3230
|
+
statements: create.map((s) => render(s, vars))
|
|
3231
|
+
},
|
|
3232
|
+
revoke: {
|
|
3233
|
+
kind: "sql",
|
|
3234
|
+
...parameters,
|
|
3235
|
+
statements: revoke.map((s) => render(s, {
|
|
3236
|
+
name: req.roleName,
|
|
3237
|
+
verifier: ""
|
|
3238
|
+
}))
|
|
3239
|
+
}
|
|
3240
|
+
};
|
|
3241
|
+
},
|
|
3242
|
+
renderRevoke(config, resourceRef) {
|
|
3243
|
+
if (config.provider !== "redis") throw new Error("redisProvider given a non-redis config");
|
|
3244
|
+
redisUserNameSchema.parse(resourceRef);
|
|
3245
|
+
return {
|
|
3246
|
+
kind: "sql",
|
|
3247
|
+
statements: statementsFor(config).revoke.map((s) => render(s, {
|
|
3248
|
+
name: resourceRef,
|
|
3249
|
+
verifier: ""
|
|
3250
|
+
}))
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
// ../../packages/core/src/providers/ssh.ts
|
|
3255
|
+
/**
|
|
3256
|
+
* Tier-1 SSH certificate-authority provider. The consumer generated an
|
|
3257
|
+
* ephemeral keypair locally and sent only its public key; `render` produces a
|
|
3258
|
+
* validated {@link SshCertSpec} that the broker's `SshCaExecutor` signs with the
|
|
3259
|
+
* CA key. So render itself is pure and secret-free — it never sees the CA key,
|
|
3260
|
+
* and the consumer's private key never leaves their machine.
|
|
3261
|
+
*
|
|
3262
|
+
* A clock-skew grace is applied to `validAfter` so freshly minted certs work on
|
|
3263
|
+
* hosts whose clocks lag slightly. Revocation is a no-op: certificates are
|
|
3264
|
+
* self-contained, so v1 relies on short TTLs (a KRL is a follow-up).
|
|
3265
|
+
*/
|
|
3266
|
+
/** Backdate the validity window so minor host clock skew doesn't reject certs. */
|
|
3267
|
+
const CLOCK_SKEW_GRACE_SECONDS = 60;
|
|
3268
|
+
const sshProvider = {
|
|
3269
|
+
kind: "ssh",
|
|
3270
|
+
zkTier: 1,
|
|
3271
|
+
render(config, req, ctx) {
|
|
3272
|
+
if (req.provider !== "ssh") throw new Error("ssh provider received a non-ssh request");
|
|
3273
|
+
const cfg = config;
|
|
3274
|
+
sshPublicKeySchema.parse(req.publicKey);
|
|
3275
|
+
const principals = req.principals.map((p) => sshPrincipalSchema.parse(p));
|
|
3276
|
+
if (cfg.allowedPrincipals?.length) {
|
|
3277
|
+
for (const p of principals) if (!cfg.allowedPrincipals.includes(p)) throw new Error(`principal "${p}" is not allowed by this target`);
|
|
3278
|
+
}
|
|
3279
|
+
const ttl = cfg.maxTtlSeconds ? Math.min(req.ttlSeconds, cfg.maxTtlSeconds) : req.ttlSeconds;
|
|
3280
|
+
const nowSec = Math.floor(ctx.nowMs / 1e3);
|
|
3281
|
+
const validAfter = nowSec - CLOCK_SKEW_GRACE_SECONDS;
|
|
3282
|
+
const validBefore = nowSec + ttl;
|
|
3283
|
+
return {
|
|
3284
|
+
resourceRef: ctx.leaseId,
|
|
3285
|
+
expiresAt: (/* @__PURE__ */ new Date(validBefore * 1e3)).toISOString(),
|
|
3286
|
+
provision: {
|
|
3287
|
+
kind: "ssh-sign",
|
|
3288
|
+
spec: {
|
|
3289
|
+
userPublicKey: req.publicKey,
|
|
3290
|
+
keyId: ctx.leaseId,
|
|
3291
|
+
principals,
|
|
3292
|
+
validAfter,
|
|
3293
|
+
validBefore,
|
|
3294
|
+
extensions: cfg.extensions
|
|
3295
|
+
}
|
|
3296
|
+
},
|
|
3297
|
+
revoke: { kind: "noop" }
|
|
3298
|
+
};
|
|
3299
|
+
},
|
|
3300
|
+
renderRevoke() {
|
|
3301
|
+
return { kind: "noop" };
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
2451
3304
|
/**
|
|
2452
3305
|
* The one-time host setup an admin runs so a target's certificates are accepted.
|
|
2453
3306
|
* Analogue of `postgresGroupBootstrapSql` — displayed for the admin to run, not
|
|
@@ -2467,6 +3320,57 @@ function sshHostSetupInstructions(config) {
|
|
|
2467
3320
|
"sudo systemctl reload sshd"
|
|
2468
3321
|
].join("\n");
|
|
2469
3322
|
}
|
|
3323
|
+
// ../../packages/core/src/providers/index.ts
|
|
3324
|
+
/** Registry — new providers register here so callers resolve by kind. */
|
|
3325
|
+
const leaseProviders = {
|
|
3326
|
+
postgres: postgresProvider,
|
|
3327
|
+
mysql: mysqlProvider,
|
|
3328
|
+
redis: redisProvider,
|
|
3329
|
+
ssh: sshProvider,
|
|
3330
|
+
aws: awsProvider,
|
|
3331
|
+
gcp: gcpProvider,
|
|
3332
|
+
mongodb: mongoProvider,
|
|
3333
|
+
elasticsearch: managedLeaseProvider("elasticsearch"),
|
|
3334
|
+
clickhouse: managedLeaseProvider("clickhouse"),
|
|
3335
|
+
rabbitmq: managedLeaseProvider("rabbitmq"),
|
|
3336
|
+
milvus: managedLeaseProvider("milvus"),
|
|
3337
|
+
"mongodb-atlas": managedLeaseProvider("mongodb-atlas"),
|
|
3338
|
+
couchbase: managedLeaseProvider("couchbase"),
|
|
3339
|
+
snowflake: managedLeaseProvider("snowflake"),
|
|
3340
|
+
elasticache: managedLeaseProvider("elasticache"),
|
|
3341
|
+
memorydb: managedLeaseProvider("memorydb"),
|
|
3342
|
+
openrouter: managedLeaseProvider("openrouter"),
|
|
3343
|
+
tailscale: managedLeaseProvider("tailscale")
|
|
3344
|
+
};
|
|
3345
|
+
function leaseProviderFor(kind) {
|
|
3346
|
+
return leaseProviders[kind];
|
|
3347
|
+
}
|
|
3348
|
+
// ../../packages/core/src/provisioners.ts
|
|
3349
|
+
const provisionerIdSchema = z$1.string().regex(/^prv_[A-Za-z0-9_-]{32}$/);
|
|
3350
|
+
z$1.object({
|
|
3351
|
+
id: provisionerIdSchema,
|
|
3352
|
+
name: z$1.string().trim().min(1).max(128),
|
|
3353
|
+
transport: z$1.enum(["pull", "push"]).default("pull"),
|
|
3354
|
+
provisionerUrl: managedEndpointSchema.optional(),
|
|
3355
|
+
publicKeyJwk: p256PublicKeyJwkSchema.refine((value) => !("d" in JSON.parse(value)), "Supply only the public key"),
|
|
3356
|
+
wrappedSigningKey: z$1.string().max(16384).startsWith("wd1."),
|
|
3357
|
+
bootstrap: z$1.string().max(4096).startsWith("pb1.")
|
|
3358
|
+
}).strict().superRefine((v, ctx) => {
|
|
3359
|
+
if (v.transport === "push" && !v.provisionerUrl) ctx.addIssue({
|
|
3360
|
+
code: "custom",
|
|
3361
|
+
path: ["provisionerUrl"],
|
|
3362
|
+
message: "Inbound mode requires an HTTPS endpoint."
|
|
3363
|
+
});
|
|
3364
|
+
if (v.transport === "pull" && v.provisionerUrl) ctx.addIssue({
|
|
3365
|
+
code: "custom",
|
|
3366
|
+
path: ["provisionerUrl"],
|
|
3367
|
+
message: "Outbound mode does not use an endpoint."
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
function provisionerIdFor(config) {
|
|
3371
|
+
if (!config || typeof config !== "object" || !("executor" in config) || config.executor !== "remote" || !("provisionerId" in config)) return void 0;
|
|
3372
|
+
return typeof config.provisionerId === "string" ? config.provisionerId : void 0;
|
|
3373
|
+
}
|
|
2470
3374
|
// ../../packages/core/src/slug.ts
|
|
2471
3375
|
/**
|
|
2472
3376
|
* The slug rule, with no dependencies — so a caller that cannot afford zod (the
|
|
@@ -2547,6 +3451,12 @@ const AUDIT_ACTIONS = [
|
|
|
2547
3451
|
"lease.created",
|
|
2548
3452
|
"lease.revoked",
|
|
2549
3453
|
"lease.expired",
|
|
3454
|
+
"lease.provisioner_job_claimed",
|
|
3455
|
+
"lease.provisioner_registered",
|
|
3456
|
+
"lease.provisioner_access_granted",
|
|
3457
|
+
"lease.provisioner_access_revoked",
|
|
3458
|
+
"lease.provisioner_deleted",
|
|
3459
|
+
"lease.provisioner_job_completed",
|
|
2550
3460
|
"log_sink.configured",
|
|
2551
3461
|
"log_sink.deleted",
|
|
2552
3462
|
"log_sink.test",
|
|
@@ -5552,6 +6462,15 @@ function policySignerJwk(publicKeyJwk) {
|
|
|
5552
6462
|
y: jwk.y
|
|
5553
6463
|
};
|
|
5554
6464
|
}
|
|
6465
|
+
// ../../packages/crypto/src/provisioner.ts
|
|
6466
|
+
async function sealProvisionerTarget(publicKeyJwk, input) {
|
|
6467
|
+
const wrapped = await wrapDek(utf8Encode(JSON.stringify({
|
|
6468
|
+
version: 2,
|
|
6469
|
+
...input
|
|
6470
|
+
})), publicKeyJwk);
|
|
6471
|
+
if (wrapped.length > 49152) throw new Error("Target configuration is too large (48 KiB encrypted limit).");
|
|
6472
|
+
return wrapped;
|
|
6473
|
+
}
|
|
5555
6474
|
// ../../packages/crypto/src/shamir.ts
|
|
5556
6475
|
/**
|
|
5557
6476
|
* Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
|
|
@@ -6140,7 +7059,7 @@ async function createAgentTaskToken() {
|
|
|
6140
7059
|
};
|
|
6141
7060
|
}
|
|
6142
7061
|
// package.json
|
|
6143
|
-
var version = "2.
|
|
7062
|
+
var version = "2.8.0";
|
|
6144
7063
|
// ../../packages/api-client/src/index.ts
|
|
6145
7064
|
/**
|
|
6146
7065
|
* The proposer's note travels as a header, not a body field: an unprotected
|
|
@@ -6161,17 +7080,78 @@ var SeekritApiError = class extends Error {
|
|
|
6161
7080
|
this.code = code;
|
|
6162
7081
|
}
|
|
6163
7082
|
};
|
|
6164
|
-
var SeekritClient = class {
|
|
7083
|
+
var SeekritClient = class SeekritClient {
|
|
6165
7084
|
baseUrl;
|
|
7085
|
+
getLeaseSigner;
|
|
6166
7086
|
auth;
|
|
6167
7087
|
fetchImpl;
|
|
6168
7088
|
client;
|
|
6169
7089
|
constructor(options) {
|
|
6170
7090
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
6171
7091
|
this.auth = options.auth;
|
|
7092
|
+
this.getLeaseSigner = options.getLeaseSigner;
|
|
6172
7093
|
this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
|
|
6173
7094
|
this.client = options.client;
|
|
6174
7095
|
}
|
|
7096
|
+
/** Reuse this session while signing leases with its unlocked customer key. */
|
|
7097
|
+
withLeaseSigner(getLeaseSigner) {
|
|
7098
|
+
return new SeekritClient({
|
|
7099
|
+
baseUrl: this.baseUrl,
|
|
7100
|
+
auth: this.auth,
|
|
7101
|
+
fetch: this.fetchImpl,
|
|
7102
|
+
client: this.client,
|
|
7103
|
+
getLeaseSigner
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
getProvisionerAccess(orgId, id) {
|
|
7107
|
+
return this.request("GET", `/v1/orgs/${orgId}/leases/provisioners/${id}/access`);
|
|
7108
|
+
}
|
|
7109
|
+
grantProvisionerAccess(orgId, id, chain) {
|
|
7110
|
+
return this.request("POST", `/v1/orgs/${orgId}/leases/provisioners/${id}/access`, chain);
|
|
7111
|
+
}
|
|
7112
|
+
revokeProvisionerAccess(orgId, id, type, principalId) {
|
|
7113
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/leases/provisioners/${id}/access/${type}/${principalId}`);
|
|
7114
|
+
}
|
|
7115
|
+
async authorizeLease(orgId, input) {
|
|
7116
|
+
if (input.authorization) return input;
|
|
7117
|
+
const prep = await this.request("GET", `/v1/orgs/${orgId}/leases/targets/${input.targetId}/authorization`);
|
|
7118
|
+
if (!prep.required) return input;
|
|
7119
|
+
const token = this.auth.type === "bearer" ? this.auth.token : this.auth.type === "dynamic" ? await this.auth.getToken() : null;
|
|
7120
|
+
const signingKey = token && isServiceToken(token) ? await importPolicySigningKey(JSON.stringify(await crypto.subtle.exportKey("jwk", (await parseServiceToken(token)).privateKey))) : await this.getLeaseSigner?.();
|
|
7121
|
+
if (!signingKey) throw new Error("Unlock your Seekrit key to request a customer-provisioned lease.");
|
|
7122
|
+
const access = await verifyProvisionerChain(prep.chain, orgId, prep.provisionerId);
|
|
7123
|
+
if (!access.targets.includes("*") && !access.targets.includes(input.targetId)) throw new Error("Target is outside your provisioner access grant.");
|
|
7124
|
+
const leaseId = randomId("lea");
|
|
7125
|
+
const issuedAt = Date.now();
|
|
7126
|
+
const rendered = leaseProviderFor(input.provider).render(prep.config, input, {
|
|
7127
|
+
leaseId,
|
|
7128
|
+
nowMs: issuedAt
|
|
7129
|
+
});
|
|
7130
|
+
const expiresAt = Date.parse(rendered.expiresAt);
|
|
7131
|
+
if (expiresAt > access.expiresAt) throw new Error("Your access grant expires before this lease. Use a shorter duration or ask an administrator to renew it.");
|
|
7132
|
+
const proof = {
|
|
7133
|
+
version: 1,
|
|
7134
|
+
orgId,
|
|
7135
|
+
provisionerId: prep.provisionerId,
|
|
7136
|
+
targetId: input.targetId,
|
|
7137
|
+
provider: input.provider,
|
|
7138
|
+
leaseId,
|
|
7139
|
+
instanceId: prep.instanceId,
|
|
7140
|
+
nonce: crypto.randomUUID(),
|
|
7141
|
+
targetDigest: prep.targetDigest,
|
|
7142
|
+
action: provisionerTransportAction(rendered.provision),
|
|
7143
|
+
..."recipientPublicKey" in input ? { recipientPublicKey: input.recipientPublicKey } : {},
|
|
7144
|
+
issuedAt,
|
|
7145
|
+
expiresAt
|
|
7146
|
+
};
|
|
7147
|
+
return {
|
|
7148
|
+
...input,
|
|
7149
|
+
authorization: {
|
|
7150
|
+
...prep.chain,
|
|
7151
|
+
proof: await signProvisionerEnvelope("pr1", signingKey, proof)
|
|
7152
|
+
}
|
|
7153
|
+
};
|
|
7154
|
+
}
|
|
6175
7155
|
async request(method, path, body, extraHeaders) {
|
|
6176
7156
|
const headers = {
|
|
6177
7157
|
accept: "application/json",
|
|
@@ -6844,6 +7824,15 @@ var SeekritClient = class {
|
|
|
6844
7824
|
getLeaseBrokerKey(orgId) {
|
|
6845
7825
|
return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
|
|
6846
7826
|
}
|
|
7827
|
+
listProvisioners(orgId) {
|
|
7828
|
+
return this.request("GET", `/v1/orgs/${orgId}/leases/provisioners`);
|
|
7829
|
+
}
|
|
7830
|
+
registerProvisioner(orgId, input) {
|
|
7831
|
+
return this.request("POST", `/v1/orgs/${orgId}/leases/provisioners`, input);
|
|
7832
|
+
}
|
|
7833
|
+
deleteProvisioner(orgId, id) {
|
|
7834
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/leases/provisioners/${id}`);
|
|
7835
|
+
}
|
|
6847
7836
|
listLeaseTargets(orgId) {
|
|
6848
7837
|
return this.request("GET", `/v1/orgs/${orgId}/leases/targets`);
|
|
6849
7838
|
}
|
|
@@ -6905,7 +7894,7 @@ var SeekritClient = class {
|
|
|
6905
7894
|
return this.request("GET", `/v1/orgs/${orgId}/leases`);
|
|
6906
7895
|
}
|
|
6907
7896
|
mintLease(orgId, input) {
|
|
6908
|
-
return this.request("POST", `/v1/orgs/${orgId}/leases`,
|
|
7897
|
+
return this.authorizeLease(orgId, input).then((body) => this.request("POST", `/v1/orgs/${orgId}/leases`, body));
|
|
6909
7898
|
}
|
|
6910
7899
|
revokeLease(orgId, leaseId) {
|
|
6911
7900
|
return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
|
|
@@ -7325,15 +8314,17 @@ function tryBuildContext(dotenvVars = {}) {
|
|
|
7325
8314
|
email: devUser
|
|
7326
8315
|
};
|
|
7327
8316
|
else return null;
|
|
7328
|
-
|
|
8317
|
+
const ctx = {
|
|
7329
8318
|
client: new SeekritClient({
|
|
7330
8319
|
baseUrl: apiUrl,
|
|
7331
8320
|
auth,
|
|
7332
|
-
client: CLI_CLIENT
|
|
8321
|
+
client: CLI_CLIENT,
|
|
8322
|
+
getLeaseSigner: async () => importPolicySigningKey(JSON.stringify(await crypto.subtle.exportKey("jwk", await getPrivateKey(ctx))))
|
|
7333
8323
|
}),
|
|
7334
8324
|
auth,
|
|
7335
8325
|
apiUrl
|
|
7336
8326
|
};
|
|
8327
|
+
return ctx;
|
|
7337
8328
|
}
|
|
7338
8329
|
function buildContext() {
|
|
7339
8330
|
const ctx = tryBuildContext();
|
|
@@ -12881,21 +13872,56 @@ function dynamicTtl(value) {
|
|
|
12881
13872
|
function registerDynamicCommands(program) {
|
|
12882
13873
|
const dynamic = program.command("dynamic").description("manage provider-issued temporary credentials");
|
|
12883
13874
|
const targets = dynamic.command("target").description("manage dynamic credential targets");
|
|
12884
|
-
targets.command("add").requiredOption("--name <name>", "target name").requiredOption("--config <file>", "non-secret provider configuration JSON").option("--org <slug>", "organization").description("register a target; read its admin credential
|
|
13875
|
+
targets.command("add").requiredOption("--name <name>", "target name").requiredOption("--config <file>", "non-secret provider configuration JSON").option("--org <slug>", "organization").description("register a target; read its admin credential (or legacy provisioner signing key) from stdin").action(async (options) => {
|
|
12885
13876
|
const config = managedTargetConfigSchema.parse(JSON.parse(readFileSync(options.config, "utf8")));
|
|
12886
|
-
|
|
13877
|
+
const provisionerId = provisionerIdFor(config);
|
|
13878
|
+
const credentialLabel = config.executor === "remote" && !provisionerId ? "provisioner signing key" : "admin credential";
|
|
13879
|
+
if (process.stdin.isTTY) fail(`pipe the ${credentialLabel} on stdin`);
|
|
12887
13880
|
const secret = (await readStdin()).trim();
|
|
12888
|
-
if (!secret) fail(
|
|
13881
|
+
if (!secret) fail(`${credentialLabel} is empty`);
|
|
13882
|
+
if (config.executor === "remote" && !provisionerId) {
|
|
13883
|
+
let length = 0;
|
|
13884
|
+
try {
|
|
13885
|
+
length = atob(secret).length;
|
|
13886
|
+
} catch {}
|
|
13887
|
+
if (length < 32) fail("use a base64 signing key containing at least 32 random bytes (seekrit provisioner keygen)");
|
|
13888
|
+
}
|
|
12889
13889
|
const ctx = buildContext();
|
|
12890
13890
|
const org = await resolveOrg(ctx, options.org);
|
|
12891
|
-
const
|
|
12892
|
-
|
|
13891
|
+
const id = randomId("lt");
|
|
13892
|
+
let wrappedAdminSecret;
|
|
13893
|
+
if (provisionerId) {
|
|
13894
|
+
const { provisioners } = await ctx.client.listProvisioners(org.id);
|
|
13895
|
+
const provisioner = provisioners.find((p) => p.id === provisionerId);
|
|
13896
|
+
if (!provisioner) fail("provisioner not found");
|
|
13897
|
+
wrappedAdminSecret = await sealProvisionerTarget(provisioner.publicKeyJwk, {
|
|
13898
|
+
orgId: org.id,
|
|
13899
|
+
provisionerId,
|
|
13900
|
+
targetId: id,
|
|
13901
|
+
config,
|
|
13902
|
+
adminCredential: secret
|
|
13903
|
+
});
|
|
13904
|
+
} else {
|
|
13905
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
13906
|
+
wrappedAdminSecret = await wrapDek(new TextEncoder().encode(secret), publicKeyJwk);
|
|
13907
|
+
}
|
|
12893
13908
|
const { target } = await ctx.client.registerLeaseTarget(org.id, {
|
|
13909
|
+
id,
|
|
12894
13910
|
name: options.name,
|
|
12895
13911
|
config,
|
|
12896
13912
|
wrappedAdminSecret
|
|
12897
13913
|
});
|
|
12898
|
-
console.log(JSON.stringify(
|
|
13914
|
+
console.log(JSON.stringify(config.executor === "remote" && !provisionerId ? {
|
|
13915
|
+
...target,
|
|
13916
|
+
connectionToken: createProvisionerConnection({
|
|
13917
|
+
orgId: org.id,
|
|
13918
|
+
targetId: target.id,
|
|
13919
|
+
provider: config.provider,
|
|
13920
|
+
apiUrl: ctx.apiUrl,
|
|
13921
|
+
signingKey: secret,
|
|
13922
|
+
mode: config.transport
|
|
13923
|
+
})
|
|
13924
|
+
} : target));
|
|
12899
13925
|
});
|
|
12900
13926
|
targets.command("list").option("--org <slug>", "organization").action(async (options) => {
|
|
12901
13927
|
const ctx = buildContext();
|