@using EntKube.Web.Data @using EntKube.Web.Services @using Microsoft.EntityFrameworkCore @using EntKube.Web.Components.Pages.Shared @inject VaultService VaultService @inject TenantService TenantService @inject StorageService StorageService @inject CnpgService CnpgService @inject MongoService MongoService @inject RegisteredPostgresService RegisteredPostgresService

Secrets Vault

Encrypted secrets for apps and cluster components. Per-tenant AES-256-GCM envelope encryption with auto-unseal.

@if (!vaultInitialized) {
Vault Not Initialized

A per-tenant encryption key will be generated and sealed with the platform root key.

} else { @* --- Scope Toggle --- *@
Scope:
@* --- Selector Dropdown --- *@ @if (scope == "app") { @if (appOptions is not null && appOptions.Count > 0) {
} else { } } else if (scope == "component") { @if (componentOptions is not null && componentOptions.Count > 0) {
} else { } } else if (scope == "storage") { @if (storageLinkOptions is not null && storageLinkOptions.Count > 0) {
} else { } } else if (scope == "cnpg") { @if (cnpgClusterOptions is not null && cnpgClusterOptions.Count > 0) {
} else { } } else if (scope == "mongodb") { @if (mongoClusterOptions is not null && mongoClusterOptions.Count > 0) {
} else { } } else if (scope == "regpostgres") { @if (regPostgresOptions is not null && regPostgresOptions.Count > 0) {
} else { } } @* --- Docker Registries Panel --- *@ @if (scope == "docker") { } @* --- Secrets Panel --- *@ @if (HasSelection()) {
Secrets @if (scope == "app" && secrets?.Any(s => s.SyncToKubernetes) == true) { }
@* Add secret form — hidden for read-only DB scopes *@ @if (scope is not "mongodb" and not "regpostgres") {
} @if (!string.IsNullOrEmpty(errorMessage)) {
@errorMessage
} @if (!string.IsNullOrEmpty(successMessage)) {
@successMessage
} @* Secrets table *@ @if (secrets is not null && secrets.Count == 0) { } else if (secrets is not null) {
@if (scope is "cnpg" or "mongodb" or "regpostgres") { } @foreach (VaultSecret secret in secrets) { @if (scope is "cnpg" or "mongodb" or "regpostgres") { } @* Reveal value row *@ @if (revealedSecretId == secret.Id) { } @* Edit value row *@ @if (editSecretId == secret.Id) { } @* Version history row *@ @if (historySecretId == secret.Id) { } @if (syncConfigSecretId == secret.Id) { } }
NameDatabaseK8s Sync K8s Secret Namespace Updated Actions
@secret.Name @if (secret.CnpgDatabase is not null) { @secret.CnpgDatabase.Name } else if (secret.MongoDatabase is not null) { @secret.MongoDatabase.Name } else if (secret.RegisteredPostgresDatabase is not null) { @secret.RegisteredPostgresDatabase.Name } else { cluster } @if (secret.SyncToKubernetes) { Synced } else { Off } @(secret.KubernetesSecretName ?? "—") @(secret.KubernetesNamespace ?? "—") @secret.UpdatedAt.ToString("MMM d, HH:mm")
Value: @revealedValue
Version History @(secretVersions?.Count ?? 0)
@if (secretVersions is null) {
} else if (secretVersions.Count == 0) {

No previous versions recorded.

} else { @foreach (VaultSecretVersion ver in secretVersions) { @if (revealedVersionId == ver.Id) { } }
# Set by Date Actions
v@ver.VersionNumber @(ver.CreatedBy ?? "—") @ver.CreatedAt.ToString("MMM d, HH:mm")
Value: @if (versionRevealLoading) { } else { @revealedVersionValue }
}
@if (scope == "app" && clusterOptions is not null) {
}
}
@if (syncOutput is not null) {
@syncOutput
}
} } @code { [Parameter] public Guid TenantId { get; set; } private bool vaultInitialized; private string scope = "app"; // App scope private List<(string Label, Guid Id)>? appOptions; private Guid selectedAppId; // Component scope private List<(string Label, Guid Id)>? componentOptions; private Guid selectedComponentId; // Storage scope private List<(string Label, Guid Id)>? storageLinkOptions; private Guid selectedStorageLinkId; // CNPG cluster scope private List<(string Label, Guid Id)>? cnpgClusterOptions; private Guid selectedCnpgClusterId; // MongoDB cluster scope private List<(string Label, Guid Id)>? mongoClusterOptions; private Guid selectedMongoClusterId; // Registered Postgres scope private List<(string Label, Guid Id)>? regPostgresOptions; private Guid selectedRegPostgresInstanceId; // Cluster options (used in app-scope sync config) private List<(string Label, Guid Id)>? clusterOptions; // Secrets private List? secrets; private string newSecretName = ""; private string newSecretValue = ""; private string? errorMessage; private string? successMessage; private Guid? syncConfigSecretId; private string syncSecretName = ""; private string syncNamespace = ""; private Guid syncClusterId; // Sync-to-K8s state (app scope) private bool syncingSecrets; private string? syncOutput; // Reveal/Edit state private Guid? revealedSecretId; private string? revealedValue; private Guid? editSecretId; private string editSecretValue = ""; // Version history state private Guid? historySecretId; private List? secretVersions; private Guid? revealedVersionId; private string? revealedVersionValue; private bool versionRevealLoading; protected override async Task OnInitializedAsync() { SecretVault? vault = await VaultService.GetVaultAsync(TenantId); vaultInitialized = vault is not null; if (vaultInitialized) { await LoadOptions(); } } private async Task InitializeVault() { await VaultService.InitializeVaultAsync(TenantId); vaultInitialized = true; await LoadOptions(); } private async Task LoadOptions() { List customers = await TenantService.GetCustomersAsync(TenantId); appOptions = []; foreach (Customer customer in customers) { List apps = await TenantService.GetAppsAsync(customer.Id); foreach (Data.App app in apps) { appOptions.Add(($"{customer.Name} / {app.Name}", app.Id)); } } List clusters = await TenantService.GetClustersAsync(TenantId); clusterOptions = clusters.Select(c => (c.Name, c.Id)).ToList(); componentOptions = []; foreach (KubernetesCluster cluster in clusters) { List components = await VaultService.GetComponentsAsync(cluster.Id); foreach (ClusterComponent comp in components) { componentOptions.Add(($"{cluster.Name} / {comp.Name}", comp.Id)); } } // Load storage links for the storage secrets scope. List links = await StorageService.GetStorageLinksAsync(TenantId); storageLinkOptions = links .Select(l => ($"{l.Environment.Name} / {l.Name} ({l.Provider})", l.Id)) .ToList(); // Load CNPG clusters for the database secrets scope. List cnpgClusters = await CnpgService.GetClustersAsync(TenantId); cnpgClusterOptions = cnpgClusters .Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id)) .ToList(); // MongoDB clusters List mongoClusters = await MongoService.GetClustersAsync(TenantId); mongoClusterOptions = mongoClusters .Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id)) .ToList(); // Registered Postgres instances List regInstances = await RegisteredPostgresService.GetInstancesAsync(TenantId); regPostgresOptions = regInstances .Select(i => (i.Name, i.Id)) .ToList(); } private void SwitchScope(string newScope) { scope = newScope; secrets = null; selectedAppId = Guid.Empty; selectedComponentId = Guid.Empty; selectedStorageLinkId = Guid.Empty; selectedCnpgClusterId = Guid.Empty; selectedMongoClusterId = Guid.Empty; selectedRegPostgresInstanceId = Guid.Empty; errorMessage = null; successMessage = null; syncClusterId = Guid.Empty; syncOutput = null; // Docker registries panel manages its own state; no reset needed here. } private bool HasSelection() { return (scope == "app" && selectedAppId != Guid.Empty) || (scope == "component" && selectedComponentId != Guid.Empty) || (scope == "storage" && selectedStorageLinkId != Guid.Empty) || (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty) || (scope == "mongodb" && selectedMongoClusterId != Guid.Empty) || (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty); } private async Task LoadSecrets() { errorMessage = null; successMessage = null; if (scope == "app" && selectedAppId != Guid.Empty) { secrets = await VaultService.GetAppSecretsAsync(TenantId, selectedAppId); } else if (scope == "component" && selectedComponentId != Guid.Empty) { secrets = await VaultService.GetComponentSecretsAsync(TenantId, selectedComponentId); } else if (scope == "storage" && selectedStorageLinkId != Guid.Empty) { secrets = await VaultService.GetStorageLinkSecretsAsync(TenantId, selectedStorageLinkId); } else if (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty) { secrets = await VaultService.GetAllCnpgSecretsForClusterAsync(TenantId, selectedCnpgClusterId); } else if (scope == "mongodb" && selectedMongoClusterId != Guid.Empty) { secrets = await VaultService.GetAllMongoSecretsForClusterAsync(TenantId, selectedMongoClusterId); } else if (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty) { secrets = await VaultService.GetAllRegisteredPostgresSecretsForInstanceAsync(TenantId, selectedRegPostgresInstanceId); } else { secrets = null; } } private async Task AddSecret() { errorMessage = null; successMessage = null; try { if (scope == "app") { await VaultService.SetAppSecretAsync(TenantId, selectedAppId, newSecretName.Trim(), newSecretValue.Trim()); } else if (scope == "component") { await VaultService.SetComponentSecretAsync(TenantId, selectedComponentId, newSecretName.Trim(), newSecretValue.Trim()); } else if (scope == "storage") { await VaultService.SetStorageLinkSecretAsync(TenantId, selectedStorageLinkId, newSecretName.Trim(), newSecretValue.Trim()); } else if (scope == "cnpg") { await VaultService.SetCnpgClusterSecretAsync(TenantId, selectedCnpgClusterId, newSecretName.Trim(), newSecretValue.Trim()); } successMessage = $"Secret '{newSecretName.Trim()}' saved."; newSecretName = ""; newSecretValue = ""; await LoadSecrets(); } catch (Exception ex) { errorMessage = ex.Message; } } private async Task DeleteSecret(Guid secretId) { errorMessage = null; // Check if deletion is blocked by active storage bindings. (bool canDelete, string? reason) = await VaultService.CanDeleteSecretAsync(secretId); if (!canDelete) { errorMessage = reason; return; } await VaultService.DeleteSecretAsync(secretId); revealedSecretId = null; editSecretId = null; await LoadSecrets(); } private async Task RevealSecret(VaultSecret secret) { // Toggle off if already revealed. if (revealedSecretId == secret.Id) { revealedSecretId = null; revealedValue = null; return; } // Decrypt and reveal the value. revealedValue = await VaultService.GetSecretValueByIdAsync(secret.Id); revealedSecretId = secret.Id; } private void StartEdit(VaultSecret secret) { editSecretId = secret.Id; editSecretValue = ""; revealedSecretId = null; revealedValue = null; } private void CancelEdit() { editSecretId = null; editSecretValue = ""; } private async Task SaveEdit() { if (editSecretId is null || string.IsNullOrWhiteSpace(editSecretValue)) { return; } errorMessage = null; bool updated = await VaultService.UpdateSecretValueAsync(editSecretId.Value, editSecretValue.Trim()); if (updated) { successMessage = "Secret value updated."; } else { errorMessage = "Failed to update secret."; } editSecretId = null; editSecretValue = ""; await LoadSecrets(); } private void ToggleSync(VaultSecret secret) { if (secret.SyncToKubernetes) { _ = DisableSync(secret.Id); } else { syncConfigSecretId = secret.Id; syncSecretName = secret.KubernetesSecretName ?? ""; syncNamespace = secret.KubernetesNamespace ?? ""; syncClusterId = secret.KubernetesClusterId ?? Guid.Empty; } } private async Task DisableSync(Guid secretId) { await VaultService.ConfigureKubernetesSyncAsync(secretId, syncEnabled: false, secretName: null, ns: null); await LoadSecrets(); } private async Task SaveSync() { if (syncConfigSecretId is null) { return; } Guid? clusterId = (scope == "app" && syncClusterId != Guid.Empty) ? syncClusterId : (Guid?)null; await VaultService.ConfigureKubernetesSyncAsync( syncConfigSecretId.Value, syncEnabled: true, secretName: syncSecretName.Trim(), ns: syncNamespace.Trim(), clusterId: clusterId); syncConfigSecretId = null; await LoadSecrets(); } private async Task SyncAllAppSecrets() { if (selectedAppId == Guid.Empty) return; syncingSecrets = true; syncOutput = null; try { HelmExecutionResult result = await VaultService.SyncAppSecretsToKubernetesAsync(TenantId, selectedAppId); syncOutput = result.Output; } finally { syncingSecrets = false; } } private async Task ToggleHistory(Guid secretId) { if (historySecretId == secretId) { historySecretId = null; secretVersions = null; revealedVersionId = null; revealedVersionValue = null; return; } historySecretId = secretId; revealedVersionId = null; revealedVersionValue = null; secretVersions = null; secretVersions = await VaultService.GetSecretVersionsAsync(secretId); } private async Task RevealVersion(Guid secretId, VaultSecretVersion version) { if (revealedVersionId == version.Id) { revealedVersionId = null; revealedVersionValue = null; return; } versionRevealLoading = true; revealedVersionId = version.Id; revealedVersionValue = null; revealedVersionValue = await VaultService.GetSecretVersionValueAsync(secretId, version.Id); versionRevealLoading = false; } private async Task RollbackToVersion(Guid secretId, Guid versionId) { errorMessage = null; bool ok = await VaultService.RollbackToVersionAsync(secretId, versionId); if (ok) { successMessage = "Secret restored to selected version."; historySecretId = null; secretVersions = null; revealedVersionId = null; revealedVersionValue = null; await LoadSecrets(); } else { errorMessage = "Failed to restore version."; } } }