// 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; shares: number; threshold: number; } interface InitResponse { keys: string[]; root_token: string; } export class VaultInitialization extends pulumi.ComponentResource { public readonly unsealKeys: pulumi.Output; public readonly rootToken: pulumi.Output; public readonly url: pulumi.Output; 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 { 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 { 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; token: pulumi.Input; 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; public readonly vaultMount: vault.Mount; public readonly vaultProvider: vault.Provider; public readonly sourceCredentialsPath: pulumi.Output; public readonly path: pulumi.Output; public readonly roleId: pulumi.Output; public readonly secretId: pulumi.Output; public readonly users: { username: string; password: pulumi.Output; }[]; 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; // token: pulumi.Input; prefix: string; vaultMountId: pulumi.Input; } export class VaultExternalSecretsClusterAppRole extends pulumi.ComponentResource { public readonly path: pulumi.Output; public readonly roleId: pulumi.Output; public readonly secretId: pulumi.Output; // 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; public readonly roleId: pulumi.Output; public readonly secretId: pulumi.Output; 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, }); } }