Service Accounts and Bitwarden

July 16, 2026 · View on GitHub

Created: 2026-05-25 Status: Decision Draft — current baseline selected, follow-up automation still to be implemented Sprint: Sprint-0007 Source Plan: _Planning/Plan_AccessingBitwardenFromServiceAccounts.md Operations Runbook: Runbook-BitwardenServiceAccounts.md


Current BWS token-purpose baseline (Task 12.53)

BuildTooling's Bitwarden Secrets Manager path now uses two purpose-specific DPAPI token slots under C:\ProgramData\ATAP\BitwardenCredentials\<SamAccountName>:

Token purposeToken labelWho needs it
ReadOnlyCommonCIForBitwardenReadOnlyEvery developer or service account that reads BWS secrets
ReadWriteCommonCIForBitwardenReadWriteOnly explicitly authorized maintainer or provisioning identities that create, update, delete, or rotate BWS secrets

The current documentation baseline is "read everywhere, write only where justified." Task 12.54.a remains the live-review gate for which identities actually receive the optional ReadWrite slot on a given host.

Task 12.49 approved profile and Bitwarden scope

The complete service-account set that requires both a managed PowerShell profile and Bitwarden Secrets Manager ReadOnly access is SvcBuildMaster, SvcProGet, SvcSeq, SvcSQLServer, and SvcParityAudit. Registry discovery is an inventory input; it must not expand this approved provisioning scope without a new user decision.

Windows identityNeeds ReadOnlyNeeds ReadWriteNotes
SvcBuildMasterYesOnly if BuildMaster on that host creates or rotates BWS secretsDefault runtime/build reads
SvcProGetYesNo by defaultRuntime/package-feed reads only
SvcSeqYesNo by defaultSEQ runtime/configuration reads only
SvcSQLServerYesNo by defaultSQL Server runtime/configuration reads only
SvcParityAuditYesNo by defaultParity-audit automation reads only
Trusted maintainer developer workstationYesOptionalProvision ReadWrite only when that workstation performs secret maintenance
Non-maintainer developer workstationYesNoNormal development and validation only

See Runbook-BitwardenServiceAccounts.md for the authoritative provisioning sequence and NewComputerSetup.md for the linear onboarding checklist.


Purpose

This document records research findings, decision drivers, considered alternatives, decision outcomes, rationale, and trade-offs for the problem of providing Bitwarden vault access to long-running Windows service accounts that start at computer boot, before any interactive user logon.


Context

Several ATAP ecosystem services run as dedicated Windows service accounts:

ServiceWindows Service Account
BuildMasterSvcBuildMaster
ProGetSvcProGet
SEQ (log listener)SvcSeq
SQL ServerSvcSQLServer
Parity auditSvcParityAudit

These five services need to read secrets from Bitwarden — for example, database connection credentials, API keys, and inter-service authentication tokens. They are also the complete Task 12.49 user-scope-profile provisioning set.

Existing Infrastructure

The following cmdlets are involved:

  • Get-BitWardenCredential (in ATAP.Utilities.Security.Powershell) — Loads or creates DPAPI-encrypted credential files on disk. File paths follow the pattern <CredentialDirectory>\<COMPUTERNAME>_<USERNAME>_BW_Login_Credential.xml and <CredentialDirectory>\<COMPUTERNAME>_<USERNAME>_BW_Unlock_Credential.xml. Uses Export-Clixml / Import-Clixml (Windows DPAPI at rest).

  • Get-SecretATAP (in ATAP.Utilities.BuildTooling.PowerShell) — The vendor-agnostic wrapper used by all ATAP code to read secrets. Mandatory -SecretName, optional -SecretField (defaults to password). The wrapper resolves the active secret-store provider from $global:settings and dispatches. Only the Bitwarden provider is implemented today.

  • Get-SecretATAPBitwarden (in ATAP.Utilities.BuildTooling.PowerShell) — The Bitwarden provider implementation. Shells out to the bw CLI directly and parses the resulting JSON. Does not use Microsoft.PowerShell.SecretManagement or any Bitwarden vault extension. Reads BW_SESSION from process scope, then User scope, and verifies the session via bw status before retrieving the item.

  • Initialize-BitwardenSession (LoginScript.ps1) — Called at interactive user logon. Reads DPAPI credential files, logs in with bw login, unlocks with bw unlock --raw, and stores the resulting session token.

  • Initialize-ServiceAccountBitwardenSession (in ATAP.Utilities.BuildTooling.PowerShell) — The service-account analogue. Registered as a per-service-account At Startup scheduled task, runs as the owning service account, decrypts that account's DPAPI files, calls bw login + bw unlock --raw, and writes BW_SESSION to the User scope for that service account.

  • Refresh-BWSession (in ATAP.Utilities.BuildTooling.PowerShell) — Called on a recurring scheduled-task trigger. Calls bw unlock --check; if locked or expired, re-runs the login/unlock sequence and rewrites the User-scope BW_SESSION.

  • Update-ServiceAccountBWCredentialFile (in ATAP.Utilities.BuildTooling.PowerShell) — Replaces the DPAPI credential files for a service account when the Bitwarden master password, the service-account Windows password, or the host changes. Wraps Get-BitWardenCredential -Replace and runs in the service account's security context.

Current Baseline Decisions

  • Bitwarden Password Manager CLI is the selected implementation baseline for now. The current Bitwarden organization is on the Free tier, so Bitwarden Secrets Manager machine accounts are not available today.
  • BW_SESSION for service accounts will be written only to the service account's User scope, not Machine scope. This keeps the token scoped to processes started as that service account.
  • Credential material for service accounts lives under C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\.
  • Expired sessions fail the current build step immediately with a structured error. The build should not silently attempt to repair the session in-line. The primary recovery path is the scheduled refresh task; a second ad-hoc refresh task remains a follow-up item to evaluate.
  • Alternatives are retained for future reference in ServiceAccountsAndBitwarden-AlternativesConsidered.md.

These choices move the document from open-ended research toward an implementation-ready decision record while preserving the discarded and deferred options.


R-01: Bitwarden Session Lifecycle (Audit)

Completed: 2026-05-25

How BW_SESSION is Set for Interactive Users

The session is initialized by Initialize-BitwardenSession in src\ATAP.Utilities.PowerShell\Profiles\LoginScript.ps1.

The call sequence is:

  1. Get-BitWardenCredential reads two DPAPI .xml credential files from disk and returns a hashtable with LoginCredential and UnlockCredential entries.
  2. bw status is called to check whether the CLI is already authenticated.
  3. If not authenticated, bw login <email> --passwordenv BW_PASSWORD is called, passing the login password via environment variable (immediately removed after the call).
  4. bw unlock --raw --passwordenv BW_PASSWORD is called using the master/unlock password via environment variable (immediately removed after the call).
  5. The resulting session key (raw base64 token) is written to two scopes:
    • Process scope: $env:BW_SESSION = $sessionKeyStr
    • User scope: [System.Environment]::SetEnvironmentVariable('BW_SESSION', $sessionKeyStr, 'User')

The session key observed on the current machine (utat022, user whertzing) is 88 characters long.

BW_SESSION Storage Today

ScopeHow writtenNotes
Process$env:BW_SESSION = $sessionKeyStrOnly visible to the current process and its children
User (registry)[System.Environment]::SetEnvironmentVariable('BW_SESSION', $sessionKeyStr, 'User')Persists in HKCU; visible to new processes spawned as the same user

Agent-spawned shells do not inherit $env:BW_SESSION from the interactive session. The Bitwarden instructions (Bitwarden.instructions.md) explicitly document this and recommend reading via:

$bwSession = [System.Environment]::GetEnvironmentVariable('BW_SESSION', 'User')

Token TTL and Non-Interactive Renewal

The Bitwarden CLI (bw 2026.4.1) does not embed an expiry timestamp in the token string itself. Token validity is governed by the Bitwarden server-side session timeout configured in the organization's Bitwarden settings (not exposed by the CLI help text).

Key behaviors documented by the CLI:

  • bw unlock --check — exits 0 if the vault is currently unlocked, non-zero otherwise.
  • Running bw unlock again immediately invalidates all previous session keys ("After unlocking, any previous session keys will no longer be valid.").
  • bw status returns a JSON object; the status field is "unlocked" when a valid BW_SESSION is active, "locked" when authenticated but locked, or "unauthenticated" when not logged in.
  • There is no --refresh or token-renew command; renewal requires a full bw unlock.

The CLI supports three methods for passing the master password non-interactively:

bw unlock [password]                          # password as positional arg (visible in process list)
bw unlock --passwordenv <ENV_VAR_NAME>        # password in environment variable (preferred)
bw unlock --passwordfile <path>               # password as first line of a file

bw unlock Command Signature

bw unlock [options] [password]

Unlock the vault and return a new session key.

Options:
  --check                        Check lock status.
  --passwordenv <passwordenv>    Environment variable storing your password
  --passwordfile <passwordfile>  Path to a file containing your password as its first line
  --raw                          Return only the session key (no surrounding text)
  -h, --help                     display help for command

Exit codes:

Exit CodeMeaning
0Success — session key written to stdout (with --raw)
Non-zeroFailure (wrong password, network error, CLI not logged in)

R-01 Findings Summary

  • BW_SESSION is set at interactive logon via LoginScript.ps1; service accounts that start at boot have no equivalent logon trigger.
  • Token is stored in user-scope registry (HKCU), making it available to new processes run as the same interactive user — but unavailable to a different service account user.
  • Non-interactive unlock is fully supported via --passwordenv or --passwordfile.
  • Token expiry is server-configured, not embedded in the token; the CLI provides bw unlock --check to test validity before use.
  • Re-unlocking invalidates prior tokens, so coordinated refresh is important in multi-process environments.

R-02: DPAPI Credential File Approach for Service Accounts (Evaluation)

Completed: 2026-05-25

DPAPI User-Scope Confirmation

Windows DPAPI (Export-Clixml / Import-Clixml) encrypts data using a key derived from the current Windows user's credentials and is bound to the current computer. A credential file created while running as SvcBuildmaster on utat022 can only be decrypted by SvcBuildmaster on that same host. This is a hard cryptographic guarantee — no other user account, even a local administrator, can read the plaintext.

This means:

  • Each service account needs its own credential files on each host where it runs.
  • Provisioning must occur in a process running as that service account (via RunAs, PsExec, or a bootstrapping scheduled task).
  • Migrating a service to a new host requires re-provisioning credentials on the new host.

Review of Get-BitWardenCredential for Non-Interactive Use

Code inspection of Get-BitWardenCredential.ps1 reveals the following:

AspectFinding
Interactive promptsNone. No Read-Host or Get-Credential GUI calls. Passwords are accepted only as plain-text parameters.
Required params when creatingBitWardenUserName, BitWardenLoginPassword, BitWardenUnlockPassword — all accepted as strings.
Existing-file pathIf both .xml files already exist and -Replace is not set, the function loads from disk and returns — no input needed.
Missing -NonInteractive guardIf credential files are absent and required parameters are omitted, the function throws a terminating error with a clear message. This is safe but the error message could be more actionable for automated contexts.
-Replace flagCreates date-stamped backups before overwriting; fully non-interactive.

Conclusion: Get-BitWardenCredential can be called from a service account context without modification when credential files already exist. When called for the first time (provisioning), all required parameters must be supplied by the caller — there is no interactive fallback. A dedicated -NonInteractive switch (D-03) is recommended to make the failure mode explicit rather than relying on parameter validation errors.

Credential Directory Path Assessment

The default credential directory is C:\Dropbox\Security\Credentials. This path is unsuitable for service accounts because:

  1. Dropbox synchronization software does not run under Windows service accounts (SvcBuildmaster, SvcProGet, etc.).
  2. Service accounts typically do not have a Dropbox folder.
  3. NTFS ACLs on C:\Dropbox\ may not grant access to service accounts.

Recommended alternative: C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\

This path:

  • Exists on all Windows hosts regardless of Dropbox configuration.
  • Is accessible to service accounts that have been granted read/write ACL to the folder.
  • Keeps credential files separated per service account.
  • Allows ACL hardening so only the owning service account can read its own file.

Credential File Naming

Current naming convention: <COMPUTERNAME>_<USERNAME>_BW_Login_Credential.xml

This is appropriate — it naturally separates credentials by host and by user account, which is required because DPAPI binds to both.

Provisioning Workflow Requirements

Provisioning credential files for a service account requires:

  1. A one-time interactive session running as the service account (or a RunAs call that executes the provisioning script in the service account's security context).
  2. Passing the Bitwarden email, login password, and master password as parameters.
  3. Writing the DPAPI-encrypted .xml files to the service-account-specific credential directory.

Practical provisioning methods:

MethodNotes
PsExec -u SvcBuildmaster -p <pwd> pwsh -File Provision-...ps1Requires the service account password. Works from a privileged admin session.
Task Scheduler (Run As service account, trigger once)Clean; runs in the service account context. Requires planting the provisioning script.
Ansible win_scheduled_task + win_commandIdempotent; integrates with existing IAC. Preferred for fleet deployments.

Risk Assessment

RiskDescriptionMitigation
Credential file rotationIf the Bitwarden master password changes, existing .xml files must be re-provisioned.Provisioning script with -Replace flag; Ansible playbook for fleet rotation.
Account password changesIf the service account Windows password changes, DPAPI keys are re-derived on next logon; existing .xml files become unreadable.Re-provision after any service account password change.
Host changesCredential files are host-bound. New hosts require new provisioning runs.Include provisioning in the new-host Ansible playbook.
File ACL exposureIf C:\ProgramData\ATAP\BitwardenCredentials\ has overly broad ACLs, other accounts could access the files.Harden ACLs to grant read/write only to the owning service account. Document in the runbook.
Backup filesThe -Replace path writes timestamped .bak files containing DPAPI-encrypted data alongside the main files.Backup files are equally DPAPI-protected; consider a retention policy to delete old backups.

ansibleAdmin First-Boot Provisioning: Minimal Blast Radius Model

Added: 2026-05-25 — in response to design question raised during R-02 analysis.

The Insight

The standard objection to DPAPI credential files for service accounts is: "Provisioning requires an interactive session as the service account — which is impractical on a freshly imaged machine."

However, the ATAP ecosystem already has a privileged account — ansibleAdmin — that is provisioned during the Windows first-boot setup of every new computer and is called by the central Ansible controller as part of host onboarding. This account can run PowerShell scripts in the security context of any local service account (via a scheduled task or Start-Process -Credential), which means it can create DPAPI credential files for each service account, scoped to that specific computer, without any interactive logon by a human.

Why This Gives the Smallest Possible Blast Radius

Windows DPAPI binds the encryption key to the tuple (Windows user identity, computer identity). A credential file created as SvcBuildmaster on host HOST-A can only be decrypted by SvcBuildmaster on HOST-A. The file is cryptographically useless on any other host, and useless to any other user account on HOST-A.

Contrast this with alternative approaches:

ApproachBlast Radius if Compromised
Machine-scope BW_SESSION env varAll processes on the machine can read the token
Single admin DPAPI file, shared pathAny process running as that admin can decrypt and unlock
ansibleAdmin-provisioned DPAPI fileOnly the specific service account (SvcBuildmaster) on the specific host can decrypt
Bitwarden Secrets Manager tokenSingle machine account token — if leaked, all secrets accessible to that account

The ansibleAdmin first-boot provisioning model gives per-(host, service-account) blast radius — the tightest possible scope for DPAPI-based credentials.

How It Works

sequenceDiagram
    participant AC as Central Ansible Controller
    participant AA as ansibleAdmin (new host)
    participant SA as SvcBuildmaster (new host)
    participant BW as Bitwarden Cloud

    AC->>AA: Run playbook via WinRM / SSH
    Note over AA: Running as ansibleAdmin on the new host
    AA->>AA: Create SvcBuildmaster account if not present
    AA->>AA: Register scheduled task to run as SvcBuildmaster
    AA->>SA: Task runs Provision-ServiceAccountBWCredential.ps1
    Note over SA: Running in SvcBuildmaster security context
    SA->>BW: bw login + bw unlock (credentials passed as params)
    SA->>SA: Export-Clixml → COMPUTERNAME_SvcBuildmaster_BW_*.xml
    Note over SA: File encrypted by DPAPI: only SvcBuildmaster@this-host can decrypt
    SA-->>AA: Task complete
    AA->>AA: Verify ACLs on credential directory
    AA-->>AC: Provisioning complete

ansibleAdmin Bootstrap Problem (Open Question 4 — Partially Resolved)

ansibleAdmin itself needs Bitwarden access to retrieve the Bitwarden email and master password it will supply when provisioning service account credentials. Options:

Bootstrap MethodDescription
Ansible Vault variablesThe central Ansible controller stores Bitwarden credentials in an Ansible-encrypted vault file (group_vars/all/vault.yml). The controller decrypts at run time and passes as extra_vars to ansibleAdmin. Preferred.
ansibleAdmin own DPAPI fileProvisioned manually once per new host type (or automatically via the Windows Unattend.xml first-logon script). Subsequent Ansible runs use the on-disk file.
Environment variable from controllerThe controller sets BWS_ACCESS_TOKEN or BW_SESSION as a WinRM session variable when invoking the playbook. Works for ephemeral provisioning sessions.

The Ansible Vault approach (first row) is recommended because:

  • The Bitwarden credentials live in one well-secured place (the Ansible controller's vault).
  • The controller decrypts them at run time in memory; they are never written to disk on the provisioned host.
  • ansibleAdmin only needs the credentials transiently — they are passed as parameters to the provisioning script and discarded once DPAPI credential files are created.

Comparison to the Alternative That Requires a Single Shared Credential

Any approach that uses a single session token or credential shared across multiple service accounts (e.g., machine-scope BW_SESSION) trades operational simplicity for a much wider blast radius:

  • If the shared token is compromised, every service on the host can access every secret that token has access to.
  • Rotation requires coordinated restart of all services that depend on the shared token.
  • Access logging cannot distinguish which service account accessed which secret.

The ansibleAdmin per-account provisioning model avoids all of these problems at the cost of one additional step in the host onboarding Ansible playbook. Given that the playbook already provisions service accounts, this step is a natural fit.

Since credential files are per-(host, service-account), they should live under a path that reflects this and is accessible only by the owning service account:

C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\
    <COMPUTERNAME>_<ServiceAccount>_BW_Login_Credential.xml
    <COMPUTERNAME>_<ServiceAccount>_BW_Unlock_Credential.xml

ansibleAdmin creates the directory, provisions the files (running as the service account via scheduled task), then hardens ACLs to deny access to all accounts except the owning service account and SYSTEM/local Administrators.


R-03: Official Bitwarden and Bitwarden-CLI Best Practices for CI/CD (Survey)

Completed: 2026-05-25

Bitwarden Official Guidance for Headless/Server-Side Access

Bitwarden provides two distinct product lines for automated access:

Bitwarden Password Manager CLI

The personal-vault CLI (bw) requires an interactive bw login + bw unlock step to obtain a BW_SESSION token. The token grants access to the authenticated user's vault. This model is inherently session-based:

  • Designed for developer workstations, not long-running services.
  • Token lifetime is governed by the server-side "vault timeout" policy set per user.
  • Non-interactive usage is supported via --passwordenv / --passwordfile flags, but a valid session must still be established via a full bw unlock before any secret retrieval.

Bitwarden Secrets Manager (Machine Accounts / Access Tokens)

Bitwarden Secrets Manager is the architecturally cleaner option for service accounts but is not available on the current Bitwarden Free tier and is therefore deferred. The full description of the Secrets Manager model, official Bitwarden guidance, pros/cons, and the revisit trigger are recorded in ServiceAccountsAndBitwarden-AlternativesConsidered.md.

Service-Specific Secret Injection Patterns

BuildMaster (Inedo)

BuildMaster has a built-in Credentials and Secrets store (accessible via Administration > Credentials & Secrets). Variables can reference credentials using $CredentialVariable. For Bitwarden integration:

  • Native plugin: No official Bitwarden plugin exists for BuildMaster.
  • Environment variable injection: A pre-build OtterScript step can call Refresh-BWSession.ps1 to set BW_SESSION, then subsequent PowerShell operations can call Get-SecretATAP.
  • BuildMaster Variables: Store non-secret configuration in BuildMaster variables; use Bitwarden only for credentials. This limits the Bitwarden session requirement to build steps that explicitly need secrets.

ProGet (Inedo)

ProGet manages package feeds and does not typically need to retrieve secrets at runtime. Secrets needed during ProGet operation (database credentials, API keys) can be:

  • Injected into the Windows service environment via a startup script.
  • Set as User-scope environment variables for SvcProGet during provisioning.

No native Bitwarden integration exists for ProGet.

Jenkins

Jenkins has a mature Credentials Plugin ecosystem:

  • The Bitwarden Jenkins Plugin exists and supports Bitwarden Password Manager (requires BW_SESSION or Bitwarden Secrets Manager access tokens depending on version).
  • Alternatively, the withCredentials step can inject secrets from Jenkins' own credential store, which can be populated from Bitwarden during provisioning.
  • For JenkinsAgentSrvAcct, the preferred pattern is to provision BW_SESSION at agent startup and use it for the duration of the build session.

Ansible (AWX / Semaphore)

Ansible Tower / AWX has a Credentials framework:

  • The HashiVault and similar integrations exist. No official Bitwarden integration exists for AWX/Semaphore.
  • Secrets can be injected into Ansible via extra vars populated from a Refresh-BWSession.ps1-style bootstrap script run before the playbook.
  • The Ansible bootstrap problem (Open Question 4) is particularly acute here: Ansible is often the tool used to provision other services, so its own credential bootstrap must be handled by a separate, manual first-run step.

R-03 Findings Summary

  • Bitwarden Secrets Manager (using bws CLI and machine access tokens) is the architecturally correct solution for service accounts — if the org plan supports it.
  • Bitwarden Password Manager CLI with DPAPI credential files and a session-refresh pattern is the viable fallback for orgs on Free/Families plans.
  • No native Bitwarden plugins exist for BuildMaster or ProGet.
  • Jenkins has a Bitwarden plugin; it can use either Password Manager sessions or Secrets Manager tokens depending on configuration.
  • Ansible has no native Bitwarden integration; environment variable injection at startup is the standard pattern.

R-04: Alternative Patterns (Evaluation)

Completed: 2026-05-25

Note: Detailed evaluations of the discarded and deferred patterns have moved to ServiceAccountsAndBitwarden-AlternativesConsidered.md. The selected pattern (Pattern 1: DPAPI + startup unlock) is documented in full below. Patterns 2–6 retain a short verdict line and a link to their full evaluation.

Pattern Comparison Matrix

PatternComplexitySecurityNon-InteractiveRequires License UpgradeRecommended
DPAPI credential files + startup unlockMediumGoodYes (with -passwordenv)NoYes (primary fallback)
Windows Credential ManagerLow–MediumGoodYesNoPossible secondary
Bitwarden Secrets ManagerLowExcellentYes (no unlock needed)Yes (Teams/Enterprise)Yes (preferred if licensed)
HashiCorp VaultHighExcellentYesNo (OSS) / Yes (Enterprise)No (over-engineered for current scale)
Per-service env-var injection at startupLowMediumYesNoPartial (complements primary)
Named pipe / local HTTPS proxyHighGoodYesNoNo (over-engineered)

Detailed Assessment

Pattern 1: DPAPI Credential Files + Startup Unlock

How it works: Each service account has DPAPI-encrypted credential files provisioned once by an admin. At boot, a Task Scheduler job (running as the service account) reads the credential files and calls bw unlock --passwordenv to obtain BW_SESSION, then writes the token to the service account's User-scope environment variable.

Pros:

  • Uses existing Get-BitWardenCredential (Security module) and Get-SecretATAP / Get-SecretATAPBitwarden (BuildTooling module) infrastructure.
  • No additional software or licenses needed.
  • DPAPI provides strong at-rest encryption tied to the service account identity.
  • Credential files are portable across the existing provisioning/Ansible workflow.

Cons:

  • Credential files must be re-provisioned after service account password changes or host changes.
  • BW_SESSION expires; a refresh mechanism (scheduled task or watchdog) is required.
  • Credential provisioning requires executing code as the service account. This is resolved by the ansibleAdmin first-boot provisioning model — see ansibleAdmin First-Boot Provisioning in R-02. The provisioning step is incorporated into the host-onboarding Ansible playbook, so no human interactive logon is required after initial host setup.
  • Dropbox-based default path is unsuitable; path must be overridden to C:\ProgramData\ATAP\BitwardenCredentials\.

Verdict: Recommended primary pattern when combined with the ansibleAdmin first-boot provisioning model. This combination yields per-(host, service-account) blast radius — the tightest achievable with the Bitwarden Password Manager CLI. Requires D-01 through D-03 and I-01 through I-02.

Pattern 2: Windows Credential Manager

Verdict: Discarded — offers no material advantage over Pattern 1. See ServiceAccountsAndBitwarden-AlternativesConsidered.md for the full evaluation.

Pattern 3: Bitwarden Secrets Manager

Verdict: Architecturally superior; deferred because the current Bitwarden organization is on the Free tier. See ServiceAccountsAndBitwarden-AlternativesConsidered.md for the full evaluation and revisit trigger.

Pattern 4: HashiCorp Vault

Verdict: Rejected — disproportionate operational overhead for current ATAP scale. See ServiceAccountsAndBitwarden-AlternativesConsidered.md for the full evaluation.

Pattern 5: Per-Service Env-Var Injection at Startup

Verdict: Rejected — machine-scope BW_SESSION has unacceptable blast radius for production multi-service hosts. See ServiceAccountsAndBitwarden-AlternativesConsidered.md for the full evaluation.

Pattern 6: Named Pipe / Local HTTPS Proxy

Verdict: Rejected — adds custom infrastructure and a new single point of failure without enough benefit. See ServiceAccountsAndBitwarden-AlternativesConsidered.md for the full evaluation.

R-04 Findings Summary

Selected pattern: Pattern 1 (DPAPI + startup unlock) is the current baseline. Open Question 1 has been resolved — the current Bitwarden organization is on the Free tier and does not support Secrets Manager machine accounts.

Deferred pattern: Pattern 3 (Bitwarden Secrets Manager) remains the preferred future direction if the organization upgrades to a Teams or Enterprise plan.


R-05: Security and Audit Considerations

Completed: 2026-05-25

Attack Surface by Pattern

PatternCredential at RestCredential in MemoryNetwork ExposureBlast Radius
DPAPI credential filesDPAPI-encrypted .xml files; user+host-boundMaster password briefly in env var during unlockBitwarden cloud API during bw unlockSingle service account
Windows Credential ManagerDPAPI-encrypted Credential Manager entrySame as aboveSame as aboveSingle service account
Bitwarden Secrets ManagerAccess token in env var (plaintext at rest)Access token in process envBitwarden Secrets Manager APISingle machine account
HashiCorp VaultAppRole secret ID (should be wrapped/short-lived)Token in env varVault serverPolicies on token
Machine-scope env-var injectionMaster password briefly in env varMachine-scope BW_SESSION visible to allBitwarden cloud APIAll processes on host
Named pipe proxyDepends on proxy designBW_SESSION in proxyLoopback onlyProxy process

DPAPI Credential File Isolation

Windows DPAPI protects credential files at the user account level:

  • The encrypted .xml files are readable (as bytes) by any process that can access the filesystem path.
  • Decryption requires the Windows logon session of the user who encrypted the file — no other account can decrypt.
  • The files should be stored in a directory with NTFS ACLs that deny read access to other user accounts, reducing the surface for file-based tampering or replacement attacks.

Recommended ACL policy for C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\:

$acl = Get-Acl 'C:\ProgramData\ATAP\BitwardenCredentials\SvcBuildmaster'
$acl.SetAccessRuleProtection($true, $false)  # disable inheritance
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'SvcBuildmaster', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($rule)
# Add SYSTEM and local Admins for manageability, no other accounts
Set-Acl 'C:\ProgramData\ATAP\BitwardenCredentials\SvcBuildmaster' $acl

Logging and Audit Requirements

All secret access from automated accounts should be logged:

EventMinimum Log LevelWhere
Credential file loaded successfullyVerbosePSFramework / SEQ
bw unlock calledVerbosePSFramework / SEQ
bw unlock succeeded (new session)ImportantPSFramework / SEQ
bw unlock failedErrorPSFramework / SEQ + Windows Event Log
Secret retrievedDebug (name only, never value)PSFramework / SEQ
Secret retrieval failedErrorPSFramework / SEQ + Windows Event Log
Session refresh triggeredVerbosePSFramework / SEQ
Session expired / --check failedImportantPSFramework / SEQ

Never log the secret value. Log only the secret name/ID and the outcome.

Plaintext Secret Prevention

Checks against current codebase:

  • Get-BitWardenCredential: Passwords received as plain-text string parameters and immediately converted to SecureString via ConvertTo-SecureString. Plain-text strings are not written to disk or logged.
  • Get-SecretATAPBitwarden: Returns a plain [string] for the requested field. Callers that need a SecureString wrap the result with ConvertTo-SecureString. The value is never logged; only the secret name and chosen field are logged.
  • LoginScript.ps1: Sets $env:BW_PASSWORD as a plain-text env var for the duration of the bw unlock call, then immediately removes it. This is the standard pattern for the Bitwarden CLI but is a brief exposure window.

Gap identified: The --passwordenv pattern requires the password to exist as a plain-text environment variable for a brief moment.

Important clarification: bw unlock --passwordfile expects the first line of the file to contain the clear-text password. A DPAPI-encrypted .xml credential file cannot be passed directly to the Bitwarden CLI and will not be understood by bw.

For high-security environments, the safer pattern is therefore:

  1. Read the DPAPI-protected credential in memory while running as the owning service account.
  2. Write the required password to a short-lived clear-text temp file inside the ACL-protected C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\ folder.
  3. Call bw unlock --passwordfile <temp-file>.
  4. Delete the temp file immediately in a finally block.

This still creates a brief clear-text window on disk, but only inside a directory already locked to the owning service account, SYSTEM, and local Administrators. It removes the need to expose the password through process environment variables.

Rotation and Refresh Strategy

Session Refresh Behavior

  • Each service account gets a scheduled refresh task that runs at boot and at a regular interval appropriate for the Bitwarden session timeout.
  • If a build or automation step discovers that BW_SESSION has expired, that step must fail immediately with a structured error message that points the operator at the refresh path.
  • The failure message should explicitly say that the scheduled task did not refresh the session in time and that the operator should investigate the scheduled-task failure.
  • A second, operator-invoked ad-hoc scheduled task is worth evaluating, but it is not part of the current baseline decision.

Rotation Triggers

TriggerRequired ActionAutomation Target
Bitwarden login password changesRecreate the DPAPI login credential file for each affected service accountManual wrapper + scheduled/on-demand task
Bitwarden unlock/master password changesRecreate the DPAPI unlock credential file for each affected service accountManual wrapper + scheduled/on-demand task
Windows service account password changesUpdate Windows service logon credentials, scheduled tasks, and then recreate all DPAPI credential filesOrchestrated rotation workflow
Host rebuild or service migrationRe-provision the entire credential directory on the new hostAnsible or manual bootstrap
Session timeout onlyRefresh BW_SESSION; no DPAPI file regeneration requiredScheduled task

PowerShell Functions Needed

The following capability set is needed to support both manual and scheduled rotation. Some pieces already exist; others still need to be written or wrapped.

Function / Script NameStatusPurpose
Get-BitWardenCredentialExistsCreate or load the DPAPI-backed login and unlock credential files
Get-SecretATAPExists (Sprint-0007)Vendor-agnostic wrapper that dispatches to the provider chosen by $global:settings
Get-SecretATAPBitwardenExists (Sprint-0007)Bitwarden provider implementation; shells out to bw CLI directly (no SecretManagement)
Initialize-ServiceAccountBitwardenSessionExists (Sprint-0007)Per-service-account At Startup script: login + unlock + write User-scope BW_SESSION
Refresh-BWSessionExists (Sprint-0007)Validate BW_SESSION, re-unlock if needed, update the service-account User-scope token, emit structured errors
Update-ServiceAccountBWCredentialFileExists (Sprint-0007)Replace the per-(host, service-account) DPAPI credential files (wraps Get-BitWardenCredential -Replace)
Test-BWSessionNeeds implementationReturn a simple health signal for scheduled tasks, build steps, and monitoring
Update-WindowsServiceCredentialNeeds implementationUpdate the logon password for all Windows services using the rotated account
Update-ScheduledTaskCredentialNeeds implementationUpdate all scheduled tasks that run as the rotated service account
Test-ServiceAccountBitwardenAccessNeeds implementationSmoke-test secret access after provisioning or rotation
Invoke-ServiceAccountPasswordRotationNeeds implementationEnd-to-end workflow: rotate service password, update dependencies, regenerate DPAPI files, validate access

Places That Must Be Updated When a Service Password Rotates

When the Windows service account password changes, all of the following need to be reviewed or updated together:

  • The local Windows account password itself.
  • Every Windows service configured to log on as that account.
  • Every scheduled task configured to run as that account, including the Bitwarden session refresh task.
  • The stored source of truth for the service account password, such as Bitwarden items, Ansible Vault variables, host vars, or any other provisioning store.
  • The DPAPI-backed Bitwarden login/unlock credential files for that service account, because the old files are no longer usable after the Windows password change.
  • Any startup scripts, wrapper scripts, or service-install runbooks that embed or re-apply the service identity.
  • Validation and monitoring hooks that confirm the service can still refresh BW_SESSION and retrieve the expected secrets.

R-05 Findings Summary

  • DPAPI files are not readable by other accounts if NTFS ACLs are correctly set; the provisioning runbook must include ACL hardening steps.
  • Brief plaintext exposure of the master password via --passwordenv is an acceptable risk for non-HSM environments; document as a known trade-off.
  • Logging must use secret-name-only policy — never log secret values.
  • Rotation is scriptable for all candidate patterns; the provisioning script and Ansible playbook cover the DPAPI rotation path.
  • The machine-scope env-var injection pattern (Pattern 5) has unacceptable blast radius for production multi-service hosts and should not be used in production.

Decision Drivers

Based on the research findings, the key factors driving the architecture decision are:

  1. License tier — Bitwarden Secrets Manager (Pattern 3) is only available on Teams/Enterprise.
  2. Operational simplicity — Service account provisioning must be automatable via Ansible.
  3. Security isolation — Each service account should only access the secrets it needs.
  4. Minimal new dependencies — Prefer solutions that extend existing ATAP infrastructure.
  5. Rotation automation — Credential rotation must be scriptable without service restart.

Considered Alternatives

See R-04 for the in-document research matrix, or use the decision companion file ServiceAccountsAndBitwarden-AlternativesConsidered.md for the condensed list of discarded and deferred alternatives.

Short list:

#PatternIncluded in Short List?Reason
1DPAPI + startup unlockYesUses existing infrastructure; no license requirement
2Windows Credential ManagerNoOffers no advantage over Pattern 1
3Bitwarden Secrets ManagerYesArchitecturally superior; pending license confirmation
4HashiCorp VaultNoDisproportionate operational overhead
5Machine-scope env injectionNoUnacceptable blast radius in production
6Named pipe proxyNoOver-engineered

Decision Outcome (Current Baseline)

The current ATAP baseline is now defined as follows:

  • Implement Pattern 1 (DPAPI + startup unlock) using the Bitwarden Password Manager CLI and the per-(host, service-account) credential model.
  • Defer Bitwarden Secrets Manager for now because the current Bitwarden organization is on the Free tier and does not support Secrets Manager machine accounts.
  • Write BW_SESSION only to the service account User scope, never Machine scope.
  • Store credential files and transient password files under C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\.
  • Use ansibleAdmin first-boot provisioning when Ansible is available.
  • Support a manual local-admin bootstrap path for hosts that must be brought up before an Ansible controller exists.
  • Fail fast on expired sessions with a structured error that tells the operator to investigate the scheduled refresh task; do not hide the failure by attempting a silent repair inside the build step itself.

Bitwarden Secrets Manager remains a future improvement path, not the current design.

Setting Up Credentials for Services without Ansible

The manual (no-Ansible) provisioning runbook for SvcBuildmaster and SvcProGet lives in the new-computer setup document so the workstation onboarding flow has a single linear sequence:

→ See NewComputerSetup.md §9.4 — Manually provision DPAPI Bitwarden credentials for the service accounts.

That section is the authoritative checklist (9.4.1 → 9.4.9) and covers:

  1. Creating and ACL-ing C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\
  2. Launching a PowerShell session as the service account
  3. Generating the BW_Login_Credential.xml and BW_Unlock_Credential.xml DPAPI files
  4. Validating the resulting files
  5. Registering the per-service-account Initialize-ServiceAccountBitwardenSession startup task
  6. Registering the per-service-account Refresh-BWSession recurring task
  7. Smoke-testing secret retrieval via Get-SecretATAP

When the Ansible controller does exist, the same outcome is produced by the ansibleAdmin first-boot provisioning model described in R-02.

For the current BWS machine-token path, that manual/bootstrap flow means:

  1. Provision CommonCIForBitwardenReadOnly for every identity that reads secrets.
  2. Provision CommonCIForBitwardenReadWrite only for explicitly approved maintainer/provisioning identities on trusted hosts.
  3. Never persist BWS_ACCESS_TOKEN at User or Machine scope; it may exist only in process scope long enough to invoke bws.

Important Note about --passwordfile

Do not point bw --passwordfile at the DPAPI .xml files created by Get-BitWardenCredential. Those files are encrypted containers for PowerShell and are not readable by the Bitwarden CLI. The refresh logic in Initialize-ServiceAccountBitwardenSession and Refresh-BWSession instead decrypts in memory, emits a short-lived clear-text temp file in the protected folder, passes that path to bw, and then deletes the temp file immediately in a finally block.

Consequences and Trade-offs

Trade-offNotes
Provisioning complexityDPAPI credential files require the provisioning script to run in the service account's security context. Resolved by the ansibleAdmin first-boot provisioning model — the step is embedded in the existing host-onboarding Ansible playbook.
Session refresh overheadTask Scheduler jobs must be created and maintained per service account. Use Ansible for fleet-wide management.
Dropbox path unsuitableAll service account credential paths must override the default to C:\ProgramData\ATAP\BitwardenCredentials\.
License-gated improvementThe cleanest solution (Bitwarden Secrets Manager) is deferred until the organization upgrades beyond the Bitwarden Free tier.
Brief plaintext password exposureThe --passwordenv pattern briefly exposes the master password as a process env var. Accepted risk for current environment.
Minimal blast radius achievedThe ansibleAdmin + per-(host, service-account) DPAPI model gives the smallest possible blast radius for a Password Manager CLI-based approach. A compromised SvcProGet credential exposes only SvcProGet's secrets on that one host.
Ansible Vault dependencyThe central Ansible controller must securely manage Bitwarden credentials in its own Ansible Vault. The controller's vault key is a critical secret that must be backed up and protected separately.
Manual bootstrap still neededA small number of hosts may need a local-admin, no-Ansible bootstrap path. That procedure is documented above and referenced from NewComputerSetup.md.

Resolved Decisions

  1. License tier: The current Bitwarden organization uses the Free tier, so Secrets Manager machine accounts are not available in the current implementation window.
  2. Session token scope: BW_SESSION for service accounts will be written to the service account User scope.
  3. Credential file location: Use C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\.
  4. Ansible bootstrap problem: ansibleAdmin retrieves provisioning-time credentials from the Ansible controller's vault. For hosts without Ansible, the local-admin manual bootstrap path in this document is the approved fallback.
  5. Token expiry behavior: Fail the build step immediately with a structured error message, direct the operator to investigate the scheduled refresh task, and evaluate a separate ad-hoc refresh task as a follow-up enhancement.

Remaining Follow-up Questions

  1. Should ATAP implement a second, operator-invoked scheduled task specifically for ad-hoc session refresh after an expiry-related build failure?
  2. Which repository and module should own the refresh/rotation orchestration functions listed above: ATAP.Utilities.Security.Powershell, ATAP.Utilities.PowerShell, or a new build-tooling wrapper layer?

Future-Looking Possibilities

The current baseline is sized for the present ATAP environment (standalone Windows hosts, local service accounts, Bitwarden Free tier). Several improvements become available once the environment grows or shifts toward managed/domain operation. They are not part of the current implementation but should be re-evaluated when the relevant trigger condition is met.

Object Access Auditing on Credential Files

In a domain or otherwise managed Windows environment, Windows Security Auditing can log every successful and failed access to the DPAPI credential files and to the protected folder. This gives operators a tripwire for unexpected reads — for example, an account other than the owning service account attempting to open the credential file.

How it would work:

  • Enable the Audit Object Access policy at the domain or local Group Policy level.
  • Set SACL entries on C:\ProgramData\ATAP\BitwardenCredentials\<ServiceAccount>\ so that read/write operations by accounts other than the owner generate Security log events.
  • Forward those events to SEQ (or the central SIEM) and alert on unexpected principals.

Why this is deferred: The current ATAP hosts are standalone and do not run a domain controller or a central audit-log aggregator scaled for Security-channel volume. The benefit is small until a domain and a SIEM are in place.

Revisit trigger: ATAP adopts Active Directory or another centralized audit aggregator, or compliance requirements demand per-file access auditing.

Group Managed Service Accounts (gMSA) after Active Directory Adoption

If ATAP migrates from local-only Windows service accounts to Active Directory, the service accounts should move to group managed service accounts (gMSA). gMSA accounts:

  • Have passwords managed and rotated automatically by AD (typically every 30 days).
  • Cannot be used for interactive logon.
  • Are scoped to specific hosts via the PrincipalsAllowedToRetrieveManagedPassword list.

This eliminates the human-managed "service account password rotation" workflow described in R-05 above. DPAPI credential files would still be re-keyed when the gMSA password rotates, so the Bitwarden credential-file re-provisioning step must run on each rotation — ideally triggered by the same scheduled task that performs the gMSA-aware Bitwarden session refresh.

Combined with object access auditing, the (gMSA + DPAPI + auditing) model gives:

  • AD-managed Windows credentials (no human-managed service passwords).
  • Per-(host, service-account) DPAPI scoping for Bitwarden material.
  • Per-access logging for unexpected reads.

Why this is deferred: ATAP does not currently operate Active Directory. Adopting AD is a much larger decision than the secrets-management work in this document.

Revisit trigger: ATAP adopts Active Directory, or a compliance/audit requirement mandates managed service account credentials.


References