Skip to content

Security model

winrig administers Windows hosts over WinRM and keeps the AD password in memory. A bearer token is a key that at the same time gives access to the server and decrypts the AD password. This page describes the protections that the server applies.

Each request must present one of the two secrets. The shared WINRIG_AUTH_TOKEN uses the header path and requires X-AD-User. The profile token decrypts the profile file. A match of WINRIG_AUTH_TOKEN with the profile token stops the start. The server does not start without at least one secret (ADR-0001).

In HTTP, the secret of the current request defines the user in whose name a tool runs. The profile token gives the identity of the profile and makes the server ignore X-AD-*. The shared secret requires X-AD-User. The identity is not inherited from the task that made the MCP session. In stdio the identity equals the profile of the process.

The server checks the token before it trusts X-AD-User, because this header is the key of the password and session cache in memory. The password cache and the session cache are isolated by user (ADR-0001).

Passwords and passphrases do not go to the logs, the audit, the configuration and they do not go to a commit. Only the authenticated ciphertext of the profile is on the disk, and the key is absent on the server. A secret that a command carries must not appear in the audit in any form (ADR-0002, ADR-0008).

The profile file is corp.json in the profile directory. It holds the format version, the canonical account name, the HKDF-SHA256 salt, the XChaCha20-Poly1305 nonce and the ciphertext. The account name is bound as associated data. On Unix, the file mode is 0600 inside a directory 0700.

Each modifying operation requires the explicit confirmation of the operator; without a confirmation channel the operation does not run. The modifying tools get confirmation through MCP elicitation, and they fail closed when the client cannot ask (ADR-0006). The confirmation names the host, the user and the tool, and it names the target and the effect. A hidden value appears only as hidden. The old and new values of set_registry, the content of write_file and the command line of kill_process stay hidden. The text of a command and the text of a script appear in full, without the 200-character cap, because the operator confirms what will run. The server sanitizes each field and caps the other fields at 200 characters.

An operator can set WINRIG_CONFIRM_FALLBACK=proceed to run a modifying operation even when the client declared no elicitation channel. The relaxation attaches to the absent channel only: a user refusal, a cancel, a timeout and a transport error are never bypassed. The audit marks a run that happened without a confirmation (ADR-0013, TR-SEC-11).

The setting removes fail-closed from every modifying tool at once, deletion included. It applies to every client that lacks elicitation, and in HTTP mode that means each request that declares no channel.

Protected paths and destructive operations

Section titled “Protected paths and destructive operations”

A recursive deletion refuses the roots of volumes and the system directories, in any form of the path. It also refuses reparse points (junction or symlink). write_file rejects protected paths with the same list that guards deletion (ADR-0002, ADR-0015).

The operator can permit a write under a protected root with the allowlist WINRIG_WRITE_ALLOW_PATHS (ADR-0017 §7–13). An entry lifts the lexical refusal and the host-root refusal for write_file, delete_file and the file operations copy_file, move_file, rename_file and create_directory. The confirmation then carries an explicit warning sentence that names the class. The match is a prefix at a path-component boundary. It uses the normalized, case-folded resolved path, and it checks both resolutions. The allowlist never lifts the refusal of an 8.3 component. It also never lifts the refusal of a symbolic link or a junction, because the operation would act on the link target. A device volume path and an existing directory target are refused too. delete_directory and DR-6 do not change. The audit records the entry that permitted a protected path (§39).

Before confirmation, the host reports where the file will really land and where the system directories are. Therefore Windows on a volume other than C: is equally protected. The tool checks the administrative share \\host\C$\... as C:\..., and it protects the shares ADMIN$, PRINT$, SYSVOL and NETLOGON in full.

copy_file, move_file, rename_file and create_directory are off by default (WINRIG_ALLOW_FILE_OPS) and they obey the confirmation of DR-5. A directory copy is recursive and shows the counts. It refuses a source tree that holds a symbolic link or a junction, and the command repeats the check at run time. A move on one volume renames. Across volumes a file is copied and then deleted. A directory is refused, because the implied recursive delete falls under DR-6. The target of each tool obeys the write boundary. The source of a copy is read-only, so a read or a download from a system directory works (ADR-0017 §14–20).

download_file is off by default (WINRIG_ALLOW_FILE_DOWNLOAD). It obeys the confirmation of DR-5, because it moves data off the host. The local endpoint is WINRIG_TRANSFER_DIR, with one subdirectory per user in HTTP mode (DR-3). The directory and its files use the private modes. The tool reads the host file in windows of 16 MiB through a raw channel. The content never goes inline into the reply. The tool hashes the bytes while it reads them and confirms the whole length with a separate host pass. It takes one plain local file name. It refuses a separator, a .. component, a symbolic link, a reserved device name and a control or format character. WINRIG_MAX_TRANSFER_BYTES (256 MiB by default) refuses a larger file on the scanned length before any read. A failure or a cancel removes the partial local file. The audit records the resolved paths, the size, both hashes and the confirmation state, never the content (ADR-0017 §21–28).

upload_file is off by default (WINRIG_ALLOW_FILE_UPLOAD). It obeys the confirmation of DR-5, and the prompt shows the SHA-256 of the local file. It reads only from the transfer directory of the request user (DR-3). It refuses a symbolic link and a file that another user can write. On Unix it also refuses a file with more than one hard link. On Windows the tool cannot see a hard link, so keep the transfer directory private. The remote target obeys the write boundary of write_file, including WINRIG_WRITE_ALLOW_PATHS and the warning line. The bytes travel on the standard input of the host command, never in the command text. The tool hashes the local bytes again while it sends them, and the host hashes the temporary file before the rename. Before the placement, a failure removes the temporary file and leaves the target unchanged. A failure can be a mismatch or a change of the local file after the confirmation. A cancel stops the host command and removes the temporary file. A cancel during the placement leaves the target in an unknown state, and the audit records unknown. A server shutdown during a cancel can drop the removal task. Then the temporary file stays on the host, and the audit still records the cancel. WINRIG_MAX_TRANSFER_BYTES (256 MiB by default) refuses a larger file before the read. The audit records the two resolved paths, the size, both hashes and the confirmation state, never the content (ADR-0017 §26, §38).

run_command runs one PowerShell command on the host. It is off by default (WINRIG_ALLOW_COMMAND_EXEC) and it obeys the confirmation of DR-5. The server encodes the command text as a base64 UTF-16 string. It decodes the string into a script block on the host. A quote or a terminator in the text therefore cannot break out of the generated wrapper. The command must fit one WinRS command line, so the text holds at most 979 characters. A longer script runs through run_script.

run_script runs a PowerShell script that is longer than one WinRS command line. It is off by default (WINRIG_ALLOW_SCRIPT_EXEC) and it obeys the confirmation of DR-5. The server writes the script into a temporary .ps1 in the session temp directory in chunks. It reads the file and runs its text as a script block, then tries to delete the file. The script has no $PSScriptRoot, no $PSCommandPath and no arguments. The tool shows the SHA-256 of the script in the confirmation and records it in the audit, and it verifies the hash before the run. One confirmation covers the whole operation and shows the whole script. WINRIG_ALLOWED_COMMANDS does not filter a script: the server cannot prefix-check the text of a script. The script obeys WINRIG_MAX_WRITE_BYTES, and the output obeys the same limits as run_command (ADR-0017 §29–34).

WINRIG_ALLOWED_COMMANDS is a prefix list, matched without case after a trim. An empty list admits any command. The list is not a boundary (ADR-0013 §18). A leading &, ., powershell -c, an alias, a call operator and an encoded command all start with text the list need not contain. Any text after an allowed prefix runs. So ;, a newline, | and $(...) do not return control to the list. An entry of & or . alone admits every command. A script written with write_file and then started also goes around it. The list protects against an accidental command, not against intent.

The list of allowed hosts and the ban on the downgrade of the TLS check are configured through the environment (ADR-0004, ADR-0015).

  • By default, the server connects only to loopback: localhost, 127.0.0.0/8, ::1. It does not resolve DNS.
  • WINRIG_ALLOW_INSECURE_TLS defaults to false: the server rejects verify_cert=false before any network activity.
  • A wrong item in WINRIG_ALLOWED_HOSTS (for example *.corp.example) stops the start.

winrig serves open HTTP on /mcp; the binary has no TLS listener. The bearer token is now a key: anyone who intercepts it reaches the server and decrypts the AD password.

A multi-user deployment assumes that the proxy authenticates the clients. The proxy passes the headers Authorization, X-AD-User and X-AD-Password without changes.

The remote WinRM leg is separate: use use_ssl or port 5986 when the hop to the Windows host needs encryption. Over open HTTP, the encryption of this WinRM leg is only partial. winrm-rs seals the SOAP bodies with the NTLM session key, but the key does not exist until the handshake completes. The first request on each new connection therefore carries its body in clear text next to the Type 3 message. But if a host answers an unencrypted message with an empty HTTP 500 (AllowUnencrypted=false, the Windows default), it rejects this first request immediately. Port 5986 removes both the exposure and the refusal.

Do not open a public issue. Use the private GitHub channel in SECURITY.md. The security policy lists the accepted boundaries of the product and what to expect after a report.