This document captures the first repeatable end-to-end V1 scenario and the minimum self-hosted packaging baseline that was proven locally.
The scenario below covers:
- Installation bootstrap.
- Topology and catalog setup.
- Appsettings import at installation scope.
- Effective preview.
- Publish.
- Agent enrollment and sync.
- Local runtime consumption through the sample provider.
- Draft update.
- Rollback.
- .NET 10 SDK.
- Node.js and npm.
- Docker Desktop or another local Docker engine.
- PowerShell.
Start the local infrastructure first:
docker compose up -dDefault local endpoints:
- PostgreSQL:
localhost:5432 - Redis:
localhost:6380
Persist the ASP.NET Data Protection key ring. Draft secrets will not survive a restart safely if the control plane key ring is ephemeral.
Set-Location "O:\DEV\Projects\SecretManager"
New-Item -ItemType Directory -Force -Path ".local\control-plane\keys" | Out-Null
$env:ASPNETCORE_URLS = 'http://localhost:5205'
$env:ConnectionStrings__Postgres = 'Host=localhost;Port=5432;Database=secretmanager_dev;Username=secretmanager;Password=secretmanager'
$env:Infrastructure__Redis__Configuration = 'localhost:6380,password=secretmanager,abortConnect=false'
$env:Infrastructure__Redis__InstanceName = 'secretmanager:'
$env:Telemetry__EnableConsoleExporter = 'false'
$env:Infrastructure__DataProtection__KeyRingPath = (Resolve-Path '.local\control-plane\keys')
dotnet run --project ".\src\SecretManager.ControlPlane.Api\SecretManager.ControlPlane.Api.csproj" --urls "http://localhost:5205"If a stale build output is locked by another process, run the built DLL directly instead of dotnet run.
The UI was validated separately for bootstrap, topology, catalog, workflow, and runtime views.
Set-Location "O:\DEV\Projects\SecretManager\src\SecretManager.Web"
npm install
npx ng serve --port 4201The development proxy targets http://localhost:5204 by default. For local UI smoke against a different control-plane port, update the proxy target or run the API on the expected port.
Bootstrap a clean installation and open a cookie-backed operator session:
$bootstrapBody = @{
installationName = 'Secret Manager Local'
ownerUsername = 'rootadmin'
ownerDisplayName = 'Root Admin'
password = 'Passw0rd!Passw0rd!'
} | ConvertTo-Json
Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/auth/bootstrap' -Method Post -ContentType 'application/json' -Body $bootstrapBody
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$loginBody = @{ username = 'rootadmin'; password = 'Passw0rd!Passw0rd!' } | ConvertTo-Json
Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/auth/login' -Method Post -WebSession $session -ContentType 'application/json' -Body $loginBodyCreate the environment, node group, managed node, application, and assignment required for preview and runtime delivery:
$environment = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/environments' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
name = 'Production'
slug = 'production'
description = 'Smoke production environment'
isProtected = $true
} | ConvertTo-Json)
$nodeGroup = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/node-groups' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
environmentId = $environment.environmentId
name = 'Backend'
slug = 'backend'
description = 'Primary rollout group'
} | ConvertTo-Json)
$node = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/nodes' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
environmentId = $environment.environmentId
nodeGroupId = $nodeGroup.nodeGroupId
name = 'node-01'
hostname = 'node-01.local'
platform = 'windows'
status = 'Healthy'
lastSeenAtUtc = $null
agentVersion = 'pending'
rolloutPolicyDefault = 'immediate'
} | ConvertTo-Json)
$application = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/applications' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
name = 'Trading API'
slug = 'trading-api'
description = 'End-to-end smoke application'
defaultIntegrationMode = 'runtime-api'
} | ConvertTo-Json)
Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/application-assignments' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
applicationId = $application.applicationId
environmentId = $environment.environmentId
nodeGroupId = $nodeGroup.nodeGroupId
managedNodeId = $null
enabled = $true
} | ConvertTo-Json)Apply an appsettings-style baseline at installation scope. This is the slice that was explicitly revalidated after the installation-scope publish and preview fixes.
Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/imports/appsettings/apply' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
applicationId = $application.applicationId
scopeType = 'Installation'
scopeId = '00000000-0000-0000-0000-000000000001'
jsonPayload = @'
{
"Trading": {
"Core": {
"ApiKey": "bootstrap-secret",
"MaxRetries": 3
}
}
}
'@
secretFullPaths = @('Trading:Core:ApiKey')
changeNote = 'Imported smoke baseline'
} | ConvertTo-Json -Depth 5)Resolve the effective preview for the target node, then publish the first immutable version:
$preview = Invoke-RestMethod -Uri "http://localhost:5205/api/v1/effective-snapshots/preview?applicationId=$($application.applicationId)&environmentId=$($environment.environmentId)&managedNodeId=$($node.nodeId)" -WebSession $session
$preview.itemCount
$baselinePublish = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/publishes' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
applicationId = $application.applicationId
environmentId = $environment.environmentId
changeSummary = 'Publish imported baseline'
rolloutPolicy = 'immediate'
} | ConvertTo-Json)Issue an enrollment token for the managed node:
$token = Invoke-RestMethod -Uri "http://localhost:5205/api/v1/nodes/$($node.nodeId)/enrollment-token" -Method Post -WebSession $sessionStart the worker with isolated local state and a persisted agent key ring:
Set-Location "O:\DEV\Projects\SecretManager"
New-Item -ItemType Directory -Force -Path ".local\agent-e2e\state" | Out-Null
New-Item -ItemType Directory -Force -Path ".local\agent-e2e\keys" | Out-Null
$env:ASPNETCORE_URLS = 'http://localhost:5159'
$env:Telemetry__EnableConsoleExporter = 'false'
$env:Infrastructure__Redis__Configuration = 'localhost:6380,password=secretmanager,abortConnect=false'
$env:Infrastructure__Redis__InstanceName = 'secretmanager:'
$env:Agent__ControlPlaneBaseUrl = 'http://localhost:5205'
$env:Agent__ManagedNodeId = $node.nodeId
$env:Agent__Hostname = 'node-01.local'
$env:Agent__Platform = 'windows'
$env:Agent__AgentVersion = 'e2e-smoke'
$env:Agent__EnrollmentToken = $token.enrollmentToken
$env:Agent__EnableBackgroundSync = 'true'
$env:Agent__SyncPollIntervalSeconds = '2'
$env:Agent__EnableChangeNotifications = 'false'
$env:Agent__LocalSnapshotStore__FilePath = (Resolve-Path '.local\agent-e2e\state').Path + '\snapshots.enc.json'
$env:Agent__RegistrationState__FilePath = (Resolve-Path '.local\agent-e2e\state').Path + '\registration.protected.json'
$env:Agent__DataProtection__KeyRingPath = (Resolve-Path '.local\agent-e2e\keys')
dotnet run --project ".\src\SecretManager.Agent.Worker\SecretManager.Agent.Worker.csproj" --urls "http://localhost:5159"Read the values through the sample configuration provider:
Set-Location "O:\DEV\Projects\SecretManager"
dotnet run --project ".\samples\SecretManager.ConfigurationProvider.Sample\SecretManager.ConfigurationProvider.Sample.csproj" -- --application-slug=trading-api --base-address=http://localhost:5159 --config-key=Trading:Core:MaxRetries
dotnet run --project ".\samples\SecretManager.ConfigurationProvider.Sample\SecretManager.ConfigurationProvider.Sample.csproj" -- --application-slug=trading-api --base-address=http://localhost:5159 --config-key=Trading:Core:ApiKeyExpected baseline values:
Trading:Core:MaxRetries = 3Trading:Core:ApiKey = bootstrap-secret
Patch the installation-scope drafts, then publish a new version:
$drafts = Invoke-RestMethod -Uri "http://localhost:5205/api/v1/draft-values?applicationId=$($application.applicationId)" -WebSession $session
$configItems = Invoke-RestMethod -Uri "http://localhost:5205/api/v1/config-items?applicationId=$($application.applicationId)" -WebSession $session
$apiKeyConfigItem = $configItems | Where-Object fullPath -eq 'Trading:Core:ApiKey'
$retriesConfigItem = $configItems | Where-Object fullPath -eq 'Trading:Core:MaxRetries'
$apiKeyDraft = $drafts | Where-Object configItemId -eq $apiKeyConfigItem.configItemId
$retriesDraft = $drafts | Where-Object configItemId -eq $retriesConfigItem.configItemId
Invoke-RestMethod -Uri "http://localhost:5205/api/v1/draft-values/$($apiKeyDraft.draftValueId)" -Method Patch -WebSession $session -ContentType 'application/json' -Body (@{
valueJson = '"rotated-secret"'
changeNote = 'Rotate API key for e2e validation'
} | ConvertTo-Json)
Invoke-RestMethod -Uri "http://localhost:5205/api/v1/draft-values/$($retriesDraft.draftValueId)" -Method Patch -WebSession $session -ContentType 'application/json' -Body (@{
valueJson = '5'
changeNote = 'Raise retry limit for e2e validation'
} | ConvertTo-Json)
$updatedPublish = Invoke-RestMethod -Uri 'http://localhost:5205/api/v1/publishes' -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
applicationId = $application.applicationId
environmentId = $environment.environmentId
changeSummary = 'Rotate API key and raise retry limit'
rolloutPolicy = 'immediate'
} | ConvertTo-Json)Re-run the sample and confirm:
Trading:Core:MaxRetries = 5Trading:Core:ApiKey = rotated-secret
$versions = Invoke-RestMethod -Uri "http://localhost:5205/api/v1/published-versions?applicationId=$($application.applicationId)&environmentId=$($environment.environmentId)" -WebSession $session
$baselineVersion = $versions | Where-Object versionNumber -eq 1
Invoke-RestMethod -Uri "http://localhost:5205/api/v1/published-versions/$($baselineVersion.publishedVersionId)/rollback" -Method Post -WebSession $session -ContentType 'application/json' -Body (@{
changeSummary = 'Rollback to bootstrap baseline after e2e validation'
} | ConvertTo-Json)Re-run the sample and confirm the baseline values are restored:
Trading:Core:MaxRetries = 3Trading:Core:ApiKey = bootstrap-secret
The first self-hosted packaging baseline is intentionally simple:
- One PostgreSQL instance with persistent storage.
- One Redis instance for cache and invalidation acceleration.
- One control-plane ASP.NET Core process with a persisted Data Protection key ring.
- One Angular web build served behind a reverse proxy or static file host.
- One agent worker process per managed node with persisted registration state, encrypted snapshot state, and a persisted agent key ring.
Minimum persistence surfaces that must survive restart:
- PostgreSQL data directory.
- Control-plane Data Protection key ring.
- Agent registration state file.
- Agent encrypted snapshot store file.
- Agent Data Protection key ring.
Minimum network assumptions:
- Operators can reach the Angular UI and the control-plane API.
- Agents can make outbound calls to the control-plane API.
- Applications can reach only the node-local worker runtime API.
This baseline does not yet include:
- High availability.
- External secret stores.
- SSO.
- Approval workflows.
- Production deployment manifests beyond the service inventory and persistence requirements above.