foundation/packages/pulumi-vault/index.ts

771 lines
24 KiB
TypeScript
Raw Normal View History

// modules/vault/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as vault from "@pulumi/vault";
import { RandomPassword } from "@pulumi/random";
import { adminPolicyContent } from "./policy";
import { Secret } from "@pulumi/vault/generic";
import {
type OlsitecProjectFeatureFlags,
type OlsitecCredentialTypes,
type GitProjectCredentials,
type OciRegistryCredentials,
type MinioBackupProjectCredentials,
} from "./olsitec-types";
interface VaultInitializationArgs {
url: pulumi.Input<string>;
shares: number;
threshold: number;
}
interface InitResponse {
keys: string[];
root_token: string;
}
export class VaultInitialization extends pulumi.ComponentResource {
public readonly unsealKeys: pulumi.Output<string[] | undefined>;
public readonly rootToken: pulumi.Output<string>;
public readonly url: pulumi.Output<string>;
constructor(
name: string,
args: VaultInitializationArgs,
opts?: pulumi.ComponentResourceOptions,
) {
super("custom:resource:VaultInitialization", name, {}, opts);
const config = new pulumi.Config("vaultCredentials"); // Specify a namespace if needed
const storedUnsealKeys = config.getSecret("unsealKeys");
const storedRootToken = config.getSecret("rootToken");
this.url = pulumi.output(args.url);
const initResult = pulumi
.all([
args.url,
pulumi.output(args.shares),
pulumi.output(args.threshold),
storedUnsealKeys,
storedRootToken,
])
.apply(async ([url, shares, threshold, keys, token]) => {
// Check if it's a preview; skip initialization if so
if (pulumi.runtime.isDryRun()) {
pulumi.log.info("Skipping Vault initialization during preview.");
return { keys: [], root_token: "" };
}
// Implement polling to ensure Vault is ready
await VaultInitialization.waitForVault(url);
// Perform the initialization
pulumi.log.info(`Attempting to initialize Vault at ${url}/v1/sys/init`);
const response = await fetch(`${url}/v1/sys/init`, {
headers: {
"Content-Type": "application/json",
Referer: url,
"Referrer-Policy": "strict-origin-when-cross-origin",
},
body: JSON.stringify({
secret_shares: shares,
secret_threshold: threshold,
}),
method: "PUT",
});
if (!response.ok) {
const errorData = await response.json();
const errorMsg = errorData.errors
? errorData.errors.join(", ")
: response.statusText;
if (
response.status === 400 &&
errorMsg.toLowerCase().includes("already initialized")
) {
if (keys && token) {
pulumi.log.info(
"Vault is already initialized. Using stored keys and token.",
);
await this.unsealVault(url, JSON.parse(keys));
return {
keys: JSON.parse(keys), // Assuming keys are stored as a JSON string
root_token: token,
};
}
pulumi.log.info("Vault is already initialized.");
return { keys: [], root_token: "" };
}
throw new Error(`Failed to initialize Vault: ${errorMsg}`);
}
const result = (await response.json()) as InitResponse;
pulumi.log.info("Vault initialized successfully.");
await this.unsealVault(url, result.keys);
return {
keys: result.keys,
root_token: result.root_token,
};
});
// Assign the outputs
this.unsealKeys = initResult.apply((result) =>
pulumi.secret((result.keys as string[]) ?? []),
);
this.rootToken = initResult.apply((result) =>
pulumi.secret(result.root_token),
);
// Register outputs
this.registerOutputs({
unsealKeys: this.unsealKeys,
rootToken: this.rootToken,
});
}
/**
* Waits until the Vault service is reachable by polling the /v1/sys/init endpoint.
* Retries up to 10 times with a 2-second delay between attempts.
*/
private static async waitForVault(url: string): Promise<void> {
const maxRetries = 12;
const retryDelay = 10000; // 10 seconds
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(`${url}/v1/sys/init`, {
method: "GET",
});
if (response.ok || response.status === 403) {
// 403 means Vault is already initialized
pulumi.log.info("Vault is reachable and ready.");
return;
}
} catch (error: any) {
pulumi.log.info(
`Attempt ${attempt}: Failed to reach Vault - ${error.message}`,
);
// Ignore errors and retry
}
if (attempt < maxRetries) {
pulumi.log.info(
`Vault not ready, retrying in ${retryDelay / 1000} seconds...`,
);
await new Promise((res) => setTimeout(res, retryDelay));
} else {
throw new Error(
"Vault service is not reachable after multiple attempts.",
);
}
}
}
/**
* Unseals the Vault service using the provided unseal keys.
*/
public async unsealVault(url: string, unsealKeys: string[]): Promise<void> {
if (unsealKeys.length === 0) {
throw new Error(
"Failed to unseal Vault: insufficient unseal keys provided.",
);
}
const sealStatusResponse = await fetch(`${url}/v1/sys/seal-status`, {
method: "GET",
});
const sealStatus = await sealStatusResponse.json();
if (!sealStatus.sealed) {
pulumi.log.info("Vault is already unsealed.");
return;
}
for (const key of unsealKeys) {
const unsealResponse = await fetch(`${url}/v1/sys/unseal`, {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key }),
method: "PUT",
});
if (!unsealResponse.ok) {
throw new Error(`Failed to unseal Vault: ${unsealResponse.statusText}`);
}
const unsealResult = await unsealResponse.json();
if (!unsealResult.sealed) {
pulumi.log.info("Vault unsealed successfully.");
return;
}
}
throw new Error("Failed to unseal Vault.");
}
}
interface VaultBootstrapArgs {
prefix: string;
url: pulumi.Input<string>;
token: pulumi.Input<string>;
userNames: string[];
secrets?: {
name: string;
path?: string;
data: pulumi.Input<{ [key: string]: any }>;
disableRead?: boolean;
}[];
}
export class VaultBootstrap extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
public readonly vaultMount: vault.Mount;
public readonly vaultProvider: vault.Provider;
public readonly sourceCredentialsPath: pulumi.Output<string>;
public readonly path: pulumi.Output<string>;
public readonly roleId: pulumi.Output<string>;
public readonly secretId: pulumi.Output<string>;
public readonly users: {
username: string;
password: pulumi.Output<string>;
}[];
private readonly secrets: Secret[];
constructor(
name: string,
args: VaultBootstrapArgs,
opts?: pulumi.ComponentResourceOptions,
) {
super("custom:resource:VaultBootstrap", name, {}, opts);
this.users = [];
this.url = pulumi.output(args.url);
// pulumi.output(args.token).apply((token) => {
// console.log("VaultBootstrap: token: ", token);
// pulumi.log.info(`VaultBootstrap: token: ${token}`);
// });
this.vaultProvider = new vault.Provider(
`${args.prefix}-vault-provider`,
{
address: args.url,
token: args.token,
},
{ parent: this },
);
this.vaultMount = new vault.Mount(
`${args.prefix}-vault-mount`,
{
path: args.prefix,
type: "kv",
options: {
version: "2",
},
},
{ provider: this.vaultProvider, parent: this },
);
this.sourceCredentialsPath = this.vaultMount.path;
const sourceCredentialsSecret = new vault.generic.Secret(
`${args.prefix}-vault-source-credentials`,
{
path: `${args.prefix}/source-credentials`,
dataJson: "{}",
disableRead: true,
},
{
provider: this.vaultProvider,
parent: this,
dependsOn: this.vaultMount,
},
);
this.secrets =
args.secrets?.map((secret) => {
return new vault.generic.Secret(
`${args.prefix}-vault-secret-${secret.name}`,
{
path: secret.path ? secret.path : `${args.prefix}/${secret.name}`,
dataJson: pulumi
.output(secret.data)
.apply((data) => JSON.stringify(data)),
disableRead: secret.disableRead,
},
{
provider: this.vaultProvider,
parent: this,
},
);
}) || [];
const pulumiPolicy = new vault.Policy(
`${args.prefix}-vault-policy-pulumi`,
{
name: `pulumi-${args.prefix}-vault-policy-pulumi`,
policy: adminPolicyContent,
},
{ provider: this.vaultProvider, parent: this },
);
const pulumiAuthBackend = new vault.AuthBackend(
`${args.prefix}-vault-auth-backend-pulumi`,
{
type: "approle",
path: `pulumi-${args.prefix}-vault-auth-backend-pulumi`,
},
{
provider: this.vaultProvider,
parent: this,
},
);
const pulumiAuthBackendRole = new vault.approle.AuthBackendRole(
`${args.prefix}-vault-auth-backend-role-pulumi`,
{
backend: pulumiAuthBackend.path,
roleName: `pulumi-${args.prefix}-vault-auth-backend-role-pulumi`,
tokenPolicies: [pulumiPolicy.name],
},
{ provider: this.vaultProvider, parent: this },
);
const pulumiSecretId = new vault.approle.AuthBackendRoleSecretID(
`${args.prefix}-vault-auth-backend-role-secret-id-pulumi`,
{
backend: pulumiAuthBackend.path,
roleName: pulumiAuthBackendRole.roleName,
},
{ provider: this.vaultProvider, parent: this },
);
const userpassAuth = new vault.AuthBackend(
`${args.prefix}-vault-auth-backend-userpass`,
{
type: "userpass",
path: `userpass`,
},
{ provider: this.vaultProvider, parent: this },
);
// Create the admin policy
const adminPolicy = new vault.Policy(
`${args.prefix}-vault-policy-admin`,
{
name: `pulumi-${args.prefix}-vault-policy-admin`,
policy: adminPolicyContent,
},
{ provider: this.vaultProvider, parent: this, dependsOn: userpassAuth },
);
// Create the admin user
this.users = args.userNames.map((userName) => {
const password = new RandomPassword(`${userName}-password`, {
length: 22,
special: false,
}).result;
const endpoint = new vault.generic.Endpoint(
"admin-user",
{
path: pulumi.interpolate`auth/${userpassAuth.path}/users/${userName}`,
dataJson: pulumi
.all({
policy: adminPolicy.name,
password: password,
})
.apply((data) => {
return JSON.stringify({
policies: [data.policy],
password: data.password,
});
}),
},
{
provider: this.vaultProvider,
parent: this,
},
);
return { username: userName, password };
});
this.path = pulumiAuthBackend.path;
this.roleId = pulumiAuthBackendRole.roleId;
this.secretId = pulumiSecretId.secretId;
this.registerOutputs({
path: this.path,
roleId: this.roleId,
secretId: this.secretId,
users: this.users.map((user) => ({
username: user.username,
password: pulumi.secret(user.password),
})),
});
}
}
interface VaultExternalSecretsClusterAppRoleArgs {
// url: pulumi.Input<string>;
// token: pulumi.Input<string>;
prefix: string;
vaultMountId: pulumi.Input<string>;
}
export class VaultExternalSecretsClusterAppRole extends pulumi.ComponentResource {
public readonly path: pulumi.Output<string>;
public readonly roleId: pulumi.Output<string>;
public readonly secretId: pulumi.Output<string>;
// public readonly vaultProvider: vault.Provider;
public readonly vaultMount: vault.Mount;
constructor(
name: string,
args: VaultExternalSecretsClusterAppRoleArgs,
opts?: pulumi.ResourceOptions,
) {
super("custom:resource:VaultExternalSecretsClusterAppRole", name, {}, opts);
// this.vaultProvider = new vault.Provider(
// `${args.prefix}-vault-provider`,
// {
// address: args.url,
// token: args.token,
// },
// { parent: this },
// );
this.vaultMount = vault.Mount.get(
`${args.prefix}-vault-mount`,
args.vaultMountId,
{
path: args.prefix,
type: "kv",
options: {
version: "2",
},
},
{
// provider: this.vaultProvider,
parent: this,
},
);
// Define the policy
const paths = {
[`${args.prefix}/data`]: '["read", "list"]',
[`${args.prefix}/data/*`]: '["read", "list"]',
[`${args.prefix}/metadata`]: '["read", "list"]',
[`${args.prefix}/metadata/*`]: '["read", "list"]',
};
let policyStatements = "";
for (const [path, capabilities] of Object.entries(paths)) {
policyStatements += `
path "${path}" {
capabilities = ${capabilities}
}
`;
}
const externalSecretsPolicy = new vault.Policy(
`${args.prefix}-vault-policy-external-secrets`,
{
name: `pulumi-${args.prefix}-vault-policy-external-secrets`,
policy: policyStatements,
},
{
// provider: this.vaultProvider,
parent: this,
},
);
const externalSecretsAuthBackend = new vault.AuthBackend(
`${args.prefix}-vault-auth-backend-external-secrets`,
{
type: "approle",
path: `pulumi-${args.prefix}-vault-auth-backend-external-secrets`,
},
{
// provider: this.vaultProvider,
parent: this,
},
);
const externalSecretsAuthBackendRole = new vault.approle.AuthBackendRole(
`${args.prefix}-vault-auth-backend-role-external-secrets`,
{
backend: externalSecretsAuthBackend.path,
roleName: `pulumi-${args.prefix}-vault-auth-backend-role-external-secrets`,
tokenPolicies: [externalSecretsPolicy.name],
},
{
// provider: this.vaultProvider,
parent: this,
},
);
const externalSecretsSecretId = new vault.approle.AuthBackendRoleSecretID(
`${args.prefix}-vault-auth-backend-role-secret-id-external-secrets`,
{
backend: externalSecretsAuthBackend.path,
roleName: externalSecretsAuthBackendRole.roleName,
},
{
// provider: this.vaultProvider,
parent: this,
},
);
this.path = externalSecretsAuthBackend.path;
this.roleId = externalSecretsAuthBackendRole.roleId;
this.secretId = externalSecretsSecretId.secretId;
this.registerOutputs({
path: this.path,
roleId: this.roleId,
secretId: this.secretId,
vaultMount: this.vaultMount,
});
}
}
interface VaultProjectArgs {
// vaultProvider: vault.Provider;
// vaultMount: vault.Mount;
prefix: string;
projectName: string;
projectStage: string;
featureFlags: OlsitecProjectFeatureFlags[];
credentials: OlsitecCredentialTypes;
gitCredentials: GitProjectCredentials;
ociRegistryCredentials: OciRegistryCredentials;
minioBackupProjectCredentials?: MinioBackupProjectCredentials;
additionalPolicyPaths?: string[];
}
export class VaultProject extends pulumi.ComponentResource {
// public readonly vaultProvider: vault.Provider;
public readonly path: pulumi.Output<string>;
public readonly roleId: pulumi.Output<string>;
public readonly secretId: pulumi.Output<string>;
constructor(
name: string,
args: VaultProjectArgs,
opts?: pulumi.ResourceOptions,
) {
super("custom:resource:VaultProject", name, {}, opts);
// this.vaultProvider = args.vaultProvider;
// Define the policy
let paths = {
[`${args.prefix}/data/${args.projectName}/${args.projectStage}`]:
'["read", "list"]',
[`${args.prefix}/data/${args.projectName}/${args.projectStage}/*`]:
'["read", "list"]',
[`${args.prefix}/metadata/${args.projectName}/${args.projectStage}`]:
'["read", "list"]',
[`${args.prefix}/metadata/${args.projectName}/${args.projectStage}/*`]:
'["read", "list"]',
};
for (const path of args.additionalPolicyPaths ?? []) {
paths = {
...paths,
[`${args.prefix}/data/${path}`]: '["read", "list"]',
[`${args.prefix}/data/${path}/*`]: '["read", "list"]',
[`${args.prefix}/metadata/${path}`]: '["read", "list"]',
[`${args.prefix}/metadata/${path}/*`]: '["read", "list"]',
};
}
let policyStatements = "";
for (const [path, capabilities] of Object.entries(paths)) {
policyStatements += `
path "${path}" {
capabilities = ${capabilities}
}
`;
}
const externalSecretsPolicy = new vault.Policy(
`${name}-vault-policy-external-secrets`,
{
name: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-policy-external-secrets`,
policy: policyStatements,
},
{
// provider: this.vaultProvider
parent: this,
},
);
const externalSecretsAuthBackend = new vault.AuthBackend(
`${name}-vault-auth-backend-external-secrets`,
{
type: "approle",
path: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-auth-backend-external-secrets`,
},
{
// provider: this.vaultProvider
parent: this,
},
);
const externalSecretsAuthBackendRole = new vault.approle.AuthBackendRole(
`${name}-vault-auth-backend-role`,
{
backend: externalSecretsAuthBackend.path,
roleName: `pulumi-${args.prefix}-${args.projectName}-${args.projectStage}-vault-auth-backend-role`,
tokenPolicies: [externalSecretsPolicy.name],
},
{
// provider: this.vaultProvider
parent: this,
},
);
const externalSecretsAuthBackendRoleSecretId =
new vault.approle.AuthBackendRoleSecretID(
`${name}-vault-auth-backend-role-secret-id-external-secrets`,
{
backend: externalSecretsAuthBackend.path,
roleName: externalSecretsAuthBackendRole.roleName,
},
{
// provider: this.vaultProvider
parent: this,
},
);
// minioBackup
if (
args.featureFlags.includes("minioBackup") &&
args.minioBackupProjectCredentials
) {
const minioBackupCredentialsSecret = new vault.generic.Secret(
`${name}-secret-backup-credentials`,
{
path: `${args.prefix}/${args.projectName}/${args.projectStage}/backup-credentials`,
dataJson: pulumi
.all({
minioBackupAccessKey:
args.minioBackupProjectCredentials.minioBackupAccessKey,
minioBackupSecretKey:
args.minioBackupProjectCredentials.minioBackupSecretKey,
minioBackupEndpoint:
args.minioBackupProjectCredentials.minioBackupEndpoint,
})
.apply((data) => JSON.stringify(data)),
},
{
// provider: this.vaultProvider
parent: this,
},
);
}
// project-credentials
const projectCredentialsSecret = new vault.generic.Secret(
`${name}-secret-project-credentials`,
{
path: `${args.prefix}/${args.projectName}/${args.projectStage}/project-credentials`,
dataJson: "{}",
disableRead: true,
},
{
// provider: this.vaultProvider
parent: this,
},
);
// service-credentials
const serviceCredentialsSecret = new vault.generic.Secret(
`${name}-secret-service-credentials`,
{
path: `${args.prefix}/${args.projectName}/${args.projectStage}/service-credentials`,
dataJson: pulumi
.all({
cockroachdbAdminUser: args.credentials.cockroachdbAdminUser,
cockroachdbAdminPassword: args.credentials.cockroachdbAdminPassword,
cockroachdbServiceUser: args.credentials.cockroachdbServiceUser,
cockroachdbServicePassword:
args.credentials.cockroachdbServicePassword,
mongodbAdminUser: args.credentials.mongodbAdminUser,
mongodbAdminPassword: args.credentials.mongodbAdminPassword,
mongodbBackupUser: args.credentials.mongodbBackupUser,
mongodbBackupPassword: args.credentials.mongodbBackupPassword,
mongodbServiceUser: args.credentials.mongodbServiceUser,
mongodbServicePassword: args.credentials.mongodbServicePassword,
mongodbKeyfile: args.credentials.mongodbKeyfile,
minioAdminUser: args.credentials.minioAdminUser,
minioAdminPassword: args.credentials.minioAdminPassword,
minioServiceUser: args.credentials.minioServiceUser,
minioServicePassword: args.credentials.minioServicePassword,
rustfsAdminUser: args.credentials.rustfsAdminUser,
rustfsAdminPassword: args.credentials.rustfsAdminPassword,
rustfsServiceUser: args.credentials.rustfsServiceUser,
rustfsServicePassword: args.credentials.rustfsServicePassword,
garageRpcSecret: args.credentials.garageRpcSecret,
garageAdminToken: args.credentials.garageAdminToken,
garageServiceKeyId: args.credentials.garageServiceKeyId,
garageServiceKeySecret: args.credentials.garageServiceKeySecret,
natsToken: args.credentials.natsToken,
grafanaAdminPassword: args.credentials.grafanaAdminPassword,
postgresUser: args.credentials.postgresUser,
postgresPassword: args.credentials.postgresPassword,
postgresServiceUser: args.credentials.postgresServiceUser,
postgresServicePassword: args.credentials.postgresServicePassword,
basicAuthUser: args.credentials.basicAuthUser,
basicAuthPassword: args.credentials.basicAuthPassword,
basicAuthHtpasswd: args.credentials.basicAuthHtpasswd,
nominatimPassword: args.credentials.nominatimPassword,
})
.apply((data) => JSON.stringify(data)),
},
{
// provider: this.vaultProvider
parent: this,
},
);
// git-credentials
const gitCredentialsSecret = new vault.generic.Secret(
`${name}-secret-git-credentials`,
{
path: `${args.prefix}/${args.projectName}/${args.projectStage}/git-credentials`,
dataJson: pulumi
.all({
gitArgocdUser: args.gitCredentials.gitArgocdUser,
gitArgocdPassword: args.gitCredentials.gitArgocdToken,
})
.apply((data) => JSON.stringify(data)),
},
{
// provider: this.vaultProvider
parent: this,
},
);
// registry-credentials
const ociRegistryCredentialsSecret = new vault.generic.Secret(
`${name}-secret-oci-registry-credentials`,
{
path: `${args.prefix}/${args.projectName}/${args.projectStage}/registry-credentials`,
dataJson: pulumi
.all({
ociRegistryAddress: args.ociRegistryCredentials.ociRegistryAddress,
ociRegistryUser: args.ociRegistryCredentials.ociRegistryUser,
ociRegistryPassword:
args.ociRegistryCredentials.ociRegistryPassword,
})
.apply((data) => JSON.stringify(data)),
},
{
// provider: this.vaultProvider
parent: this,
},
);
this.path = externalSecretsAuthBackend.path;
this.roleId = externalSecretsAuthBackendRole.roleId;
this.secretId = externalSecretsAuthBackendRoleSecretId.secretId;
this.registerOutputs({
path: this.path,
roleId: this.roleId,
secretId: this.secretId,
});
}
}