Lab 06 — Secrets in a Vault, Remoting on a Constrained Endpoint¶
Setup¶
This is a reference lab — it ships a one-command environment in the companion
plaintext-labs repo at
plaintext-labs/powershell-for-security/06-secrets-remoting-jea/: a PowerShell 7 container
(mcr.microsoft.com/powershell) with PSScriptAnalyzer, Pester,
Microsoft.PowerShell.SecretManagement, and Microsoft.PowerShell.SecretStore preinstalled (all
pinned), plus the cumulative Vigil module with this module's additions and the Vigil.ReadOnly JEA
files.
git clone https://github.com/plaintext-security/plaintext-labs
cd plaintext-labs/powershell-for-security/06-secrets-remoting-jea
make up # build the pwsh + secrets + toolchain container
make shell # drop into pwsh with Vigil loaded
make demo # gate: PSScriptAnalyzer + Pester (LIVE secret round-trip + JEA .psrc/.pssc shape)
make down # stop when done
The lab builds on a shipped SecretStore vault (cross-platform, runs live in the container) and
ships authored JEA files you validate in the container and enforce on an optional Windows VM.
Honest platform scope.
SecretManagement+SecretStoreare cross-platform — you demonstrate the secret flow live on Linux. JEA enforcement and PowerShell Remoting over WinRM are Windows-only — the container validates that your.psrc/.psscare well-formed and read-only, but it cannot enforce them. Registering the endpoint and proving a non-admin session is denied everything but the hunt verbs is the optional Windows-VM step below; it is assessed-not-demonstrated on Linux.
Scenario¶
Vigil now enriches indicators against a threat feed (Module 04) and reads telemetry from Windows hosts
(Module 03) — which means it needs a credential for the feed and a way to reach remote hosts. The
copilot's instinct is to paste the token into the script and open an admin Invoke-Command. You're going
to do neither: put the secret in a vault and fetch it by name, and reach the remote host through a
constrained Vigil.ReadOnly JEA endpoint that can run only the read-only hunt verbs.
Only test systems you own or have explicit written permission to test. The secret flow runs entirely in the local lab container against a throwaway vault; the optional remoting step targets a Windows VM you own. Never register a JEA endpoint or store real secrets on a machine you aren't authorized to change.
Do¶
- [ ] Find the plaintext secret and kill it. In a scratch script, write the copilot's version first —
$token = 'abc123'(orConvertTo-SecureString -AsPlainText 'abc123') feeding the feed call. This is the anti-pattern you're removing; keep it only long enough to see why it's committed-and-published the moment it lands in git. - [ ] Stand up the vault, store the secret. Register a
SecretStorevault (Register-SecretVault … -ModuleName Microsoft.PowerShell.SecretStore), then store the token as aSecureStringwithSet-Secret(orVigil'sSet-VigilSecret, which only accepts aSecureStringso a literal can't slip through). Retrieve it withGet-Secret -Name … -Vault …. The secret now lives in the vault, not the file. - [ ] Split config from secret. Give
Vigila committedvigil.config.jsonholding only non-secret settings (vault name, secret name, feed URL, remote host). WireGet-VigilConfigto load that file and resolve the token lazily viaGet-Secret— so the config object carries no credential material until you actually call for it. Grep your own working tree: notoken,password, or-AsPlainTextliteral should appear anywhere. - [ ] Make the store non-interactive (honestly). For the container's automated
make demo, configure the store so the firstSet-Secretdoesn't prompt (Reset-SecretStore -Authentication None -Interaction Nonefor a throwaway test store, or the password-via-Import-CliXmlpattern from the automation docs for a real one). Write down which you chose and why the passwordless option is only acceptable for a disposable test vault. - [ ] Author the JEA role capability (
.psrc). CreateVigil.ReadOnly.psrcthat exposes only the read-only hunt verbs (VisibleFunctions = @('Get-VigilEvent')) plus a tiny safe read-only cmdlet set — no wildcards, noVisibleProviders, noVisibleExternalCommands, no state-changing cmdlets. Check it against Microsoft's "dangerous commands" list: nothing on the panel should be able to elevate, write, or run arbitrary code. - [ ] Author the session configuration (
.pssc). CreateVigil.ReadOnly.psscasSessionType = 'RestrictedRemoteServer'(NoLanguage),RunAsVirtualAccount = $true, and aRoleDefinitionsmap that grants a group theVigil.ReadOnlyrole. On Linux, validate the file's shape withImport-PowerShellDataFileand the lab'sPesterassertions; on Windows, addTest-PSSessionConfigurationFile(it must returnTrue). - [ ] Build
Invoke-VigilRemoteagainst the constrained endpoint. It connects withNew-PSSession -ConfigurationName 'Vigil.ReadOnly', invokes a whitelisted verb (not an arbitrary command string — noInvoke-Expression), and pulls its credential from the vault. Have it fail loudly on a non-Windows host with a pointer to the VM step, so nobody thinks the Linux container enforced JEA. - [ ] Run the gate.
make demomust pass on Linux:PSScriptAnalyzerclean, the live secret round-trip green, and the.psrc/.psscparsed and asserted read-only. The only thing it can't do is enforce JEA — which is the next step. - [ ] (Optional Windows VM) Enforce it. On a Windows host you own, register the endpoint
(
Register-PSSessionConfiguration -Name 'Vigil.ReadOnly' -Path .\Vigil.ReadOnly.pssc), connect with a non-admin account, and prove the constraint:Get-VigilEventworks;Get-Content,Set-Service, or an arbitrary scriptblock is denied. This is the assessed-not-demonstrated piece made real. - [ ] Automate & own it. Commit the SecretStore-backed config,
Invoke-VigilRemote, theVigil.ReadOnly.psrc/.pssc, and thePestertests. In the PR, note what the copilot generated, what you corrected (the plaintext secret? the wildcardVisibleCmdlets? the adminInvoke-Command?), and the one default you had to fix.
Success criteria — you're done when¶
- [ ] No plaintext secret exists anywhere in the tree — the token is stored in and fetched from the vault by name;
Get-VigilConfigresolves it lazily and the write path refuses a plaintext string. - [ ]
make demois green on Linux:PSScriptAnalyzerclean, the liveSet/Getsecret round-trip passes, and the.psrc/.psscparse and assert read-only (no wildcards, no providers, virtual account,RestrictedRemoteServer). - [ ]
Vigil.ReadOnly.psrcexposes only read-only hunt verbs;Vigil.ReadOnly.psscis aRestrictedRemoteServerrunning under a virtual account, mapping a group to the role. - [ ]
Invoke-VigilRemotetargets theVigil.ReadOnlyendpoint (not a full admin session), whitelists the verb, and fails clearly off-Windows. - [ ] (If you did the VM step) a non-admin session on the registered endpoint runs
Get-VigilEventand is denied a state-changing or arbitrary command — you saw the constraint enforced.
Deliverables¶
The cumulative Vigil with this module's additions: Get-VigilConfig + Set-VigilSecret, the committed
vigil.config.json (non-secret only), Invoke-VigilRemote, Vigil.ReadOnly.psrc, Vigil.ReadOnly.pssc,
and the Pester tests (live secret round-trip + JEA file shape). Commit all of it. Do not commit any
real secret, any vault file, any .env, or the SecretStore data — only the non-secret config and the
capability files.
AI acceleration¶
Have the copilot draft the vault call and the JEA files, then review for the two failure classes this
module exists to catch. Secrets: the tell is a legible credential — a literal, an -AsPlainText 'literal',
a PSCredential built from a hardcoded password; hold the diff to "the secret comes from Get-Secret by
name, nothing legible in source." JEA: the tell is over-exposure — VisibleCmdlets = '*', an exposed
provider or external command, a state-changing cmdlet "for convenience"; hold the .psrc to the read-only
verbs and check it against the Microsoft dangerous-commands list. The model will also cheerfully write
Invoke-Command -ComputerName … -ScriptBlock { … } as admin — insist on -ConfigurationName Vigil.ReadOnly.
Connects forward¶
The vault-backed config feeds every later module that needs a credential (Module 07's MCP surface, Module
09's supply-chain gate). The constrained-endpoint discipline is the Windows-native cousin of Track 11
(ZTNA): least privilege enforced at the boundary, not promised in the code. Module 08 will attack
PowerShell itself — and a Vigil.ReadOnly endpoint is one of the controls that blunts the attack.
Marketable proof¶
"I keep credentials out of PowerShell source with
SecretManagement/SecretStoreand expose tooling over a constrained JEA endpoint that runs only read-only verbs — least privilege enforced at the remoting boundary, not hoped for in the script."
Stretch (optional)¶
- Swap the
SecretStorevault for Azure Key Vault (register theAz.KeyVaultextension) and prove the sameVigilcode works by changing only the-Vaultname — the portability the abstraction promises. - On the VM, turn on session transcripts in the
.pssc(TranscriptDirectory) and show that every command a connecting analyst ran is recorded — the audit half of JEA that pairs with the constraint.
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).