Run a Jenkins inbound (JNLP) agent as a native Windows Service — no login session, no scheduled tasks, no manual restarts.
Jenkins inbound agents on Windows are painful: someone has to be logged in, java -jar agent.jar has to be running in a terminal, and when it crashes, nobody restarts it until builds start failing. Scheduled tasks are fragile. NSSM wrappers lack proper lifecycle management.
JenkinsAsService replaces all of that with a proper Windows Service built on .NET 10.
- Auto-start on boot — runs under a least-privilege virtual service account (
NT SERVICE\Jenkins), no interactive login required - Event-driven watchdog — detects agent death instantly (not polling), auto-recovers with exponential backoff (10s to 5min); a persistent crash-loop stops the service so Windows Service Recovery can act, while a merely-unreachable controller is retried indefinitely
- Secret protection — TPM 2.0 hardware-backed key, DPAPI machine/user-scope encryption, Windows Credential Manager, environment variables, or plaintext
- Smart jar caching + integrity — ETag conditional GET skips the download when
agent.jaris unchanged; its SHA-256 is recorded on download and re-checked before reuse, so a tampered or corrupt cached jar is re-downloaded instead of launched - HTTP resilience — Polly-based retry, circuit breaker, and timeout on all HTTP calls
- Structured logging — Serilog rolling file + Windows Event Log, with
ProcessId/MachineNameenrichment - CLEF JSON mode — machine-parseable compact log format for Seq, Datadog, or any log aggregator
- OpenTelemetry metrics — opt-in OTLP export: restart counter, SEVERE event counter, .NET runtime metrics
- Secret redaction — agent secrets are scrubbed from all log output
- 178 unit tests — xUnit + NSubstitute + FluentAssertions (incl. an end-to-end watchdog harness), CI on every push
- 6 security scans — CodeQL (C# + Actions YAML), Semgrep, Gitleaks, PSScriptAnalyzer, Dependency Review, Trivy; all actions SHA-pinned
- Single-file deploy — self-contained
.exewith R2R, compression, and embedded PDB symbols - Dual-arch releases — x64 + x86 MSI installers, 7z/RAR archives, SHA256 checksums
flowchart TD
A[Windows Service Manager] -->|ExecuteAsync| B[Validate settings + resolve Java]
B -->|Invalid config| D[Log error and StopApplication]
B -->|OK| E[Supervision loop]
E --> F{Agent running?}
F -->|No — bring-up| G["Backoff, then connect → download agent.jar ETag → start"]
G -->|Unreachable or start fails| H[Retry forever — does not count toward MaxRetries]
H --> E
G -->|Started| E
F -->|Yes| I[Await exit signal or 60s stability]
I -->|Stable 60s| J[Reset crash counter]
J --> E
I -->|Crash| K[Log SEVERE count, kill, auto-transport fallback]
K --> L{Crashes exceed MaxRetries?}
L -->|Yes| M[StopApplication so SCM can recover]
L -->|No| E
- Windows 10 / Server 2016 or later
- A supported JDK or OpenJDK — set
JAVA_HOMEor configure the path in the installer - A Jenkins controller with an inbound (JNLP) agent node configured
- Download the
.msifor your architecture from Releases - Run the installer — it walks you through install path, Jenkins URL, agent secret, and secret protection mode
- Done — the service registers and starts automatically
The default install path is C:\Program Files\Jenkins (customizable in the UI). No .NET runtime needed — the binary is fully self-contained.
For automated deployments, use msiexec with public properties:
msiexec /i JenkinsAsService_x64.msi /qn `
INSTALLFOLDER="D:\Jenkins" `
JENKINS_URL="https://jenkins.example.com:8443" `
JENKINS_SECRET="your-secret" `
JENKINS_SECRET_MODE="Dpapi" `
JENKINS_AGENT_NAME="" `
JENKINS_JAVA_PATH=""| Property | Required | Default | Description |
|---|---|---|---|
INSTALLFOLDER |
No | C:\Program Files\Jenkins |
Installation directory |
JENKINS_URL |
Yes | — | Jenkins controller URL with explicit port |
JENKINS_SECRET |
Yes | — | JNLP agent secret |
JENKINS_SECRET_MODE |
No | Dpapi |
Dpapi, Tpm, CredentialManager, EnvironmentVariable, or Unprotected |
JENKINS_AGENT_NAME |
No | Hostname | Agent node name in Jenkins |
JENKINS_JAVA_PATH |
No | JAVA_HOME |
Path to JDK bin folder |
For environments where MSI installation isn't possible, download the .7z or .rar archive from Releases:
- Extract to a folder of your choice (e.g.
D:\Jenkins) - Edit
appsettings.json— fill inConnection:Url,Secret:Value, and any other settings - Register and start the service:
# Least-privilege virtual service account (matches the MSI default). Use obj= LocalSystem only if required.
sc.exe create Jenkins binPath= "D:\Jenkins\JenkinsAsService.exe" start= auto obj= "NT SERVICE\Jenkins"
sc.exe failure Jenkins reset= 86400 actions= restart/10000/restart/10000/restart/10000
sc.exe start JenkinsA low-privilege account cannot create the Event Log source or write into
Program Files. Runupdate-secret --silentonce from an elevated prompt (it pre-creates the source) and grant the account Modify on the install folder. The service falls back to file-only logging if the Event Log source is unavailable.
To configure secret protection, run the CLI before starting:
.\JenkinsAsService.exe update-secret --secret "your-secret" --url "https://jenkins:8443" --mode Dpapi --silentTo uninstall:
sc.exe stop Jenkins
sc.exe delete JenkinsAll settings live in the Jenkins section of appsettings.json, grouped into topic sub-sections (Connection, Secret, Agent, Hardening, Logging, Recovery). Keys below are written as Section:Key.
| Setting | Required | Default | Description |
|---|---|---|---|
Connection:Url |
Yes | — | Full URL with an explicit port. A URL with no port is rejected; an explicit standard :443 (e.g. a reverse-proxied controller) is accepted. Non-HTTPS URLs are allowed but warned (secret sent unencrypted) |
Connection:Method |
No | Auto |
Agent transport: Auto (WebSocket first, fall back to direct TCP inbound), WebSocket, or Https (direct TCP inbound) |
Connection:AgentName |
No | Hostname | Node name in Jenkins (case-sensitive) |
Connection:ControllerCertThumbprint |
No | (empty) | SHA-256 thumbprint to pin the controller TLS cert (empty = chain validation) |
Secret:Value |
Yes | — | JNLP secret (or ciphertext / env-var name / credential target, per Secret:Mode) |
Secret:Mode |
No | Unprotected |
Unprotected, Dpapi, Tpm, EnvironmentVariable, or CredentialManager |
Secret:DpapiScope |
No | Machine |
Machine or User (only when Secret:Mode is Dpapi) |
Secret:ViaFile |
No | true |
Pass the secret as -secret @<file> (ACL-restricted) instead of inline, keeping it out of the process table |
Agent:JavaPath |
No | JAVA_HOME |
Path to JDK bin folder |
Agent:CustomArguments |
No | (empty) | Extra java.exe args (supports quoted values and escaped quotes) |
Agent:DataDirectory |
No | %ProgramData%\JenkinsAsService |
Writable root for runtime data, separate from the read-only install folder: logs + secret at the root, cached agent.jar under agent\, Jenkins -workDir under work\ |
Hardening:SanitizeEnvironment |
No | true |
Launch the agent with a deny-by-default environment (curated allow-list only) |
Hardening:AllowedEnvironmentVariables |
No | (empty) | Extra env var names (;/,-separated) to pass through when sanitizing |
Logging:DebugMode |
No | false |
Verbose Java agent output in logs |
Logging:CompactLog |
No | false |
CLEF JSON output (agent.clef) instead of human-readable (agent.log) |
Logging:RetainedLogs |
No | 3 |
Number of rolled log files to keep. Oldest are permanently deleted. |
Recovery:MaxRetries |
No | 0 |
Max consecutive agent crashes before the service stops itself for SCM recovery. Unreachable-controller retries don't count. 0 = infinite |
Upgrades: An in-place MSI upgrade reconciles
appsettings.jsonto the new version's schema — settings introduced in the release appear at their defaults, settings the schema no longer defines are pruned, and your existing values and secret are preserved. The secret is detected without decryption, so User-scope DPAPI / TPM / Credential Manager secrets (bound to the service account) survive untouched. Because unknown keys are removed, configure only documented settings.
Avoid storing plaintext secrets in config. Use update-secret to write the secret in your preferred mode:
JenkinsAsService.exe update-secret --secret "your-secret" --url "https://jenkins:8443" --mode Dpapi --silent| Mode | What Secret:Value contains |
Resolution |
|---|---|---|
Unprotected |
Plaintext | Returned as-is |
Dpapi |
Base64 DPAPI ciphertext | ProtectedData.Unprotect (machine-scoped, non-portable) |
Tpm |
Base64 RSA ciphertext | RSACng decrypt via the TPM Platform Crypto Provider (non-exportable, machine-bound key) |
EnvironmentVariable |
Env var name | Reads machine-level environment variable (default: JENKINS_SECRET) |
CredentialManager |
Target name | Reads from Windows Credential Manager |
The Telemetry section is included in appsettings.json with Enabled set to false. To opt in, set Enabled to true and configure the OTLP endpoint:
{
"Telemetry": {
"Enabled": true,
"OtlpEndpoint": "http://localhost:4317",
"ServiceName": "JenkinsAsService"
}
}Exports: jenkins_agent_restarts_total, jenkins_agent_severe_events_total, and .NET runtime metrics (GC, threads, memory).
File: agent.log (human-readable) or agent.clef (CLEF JSON when CompactLog: true). Rolls at 10MB, keeps RetainedLogs backups (default: 3). Oldest files are permanently deleted.
Windows Event Log: Warnings and errors under source JenkinsAsService — crash evidence even when the file log is unavailable.
# Tail the log (in the data folder, not the install folder)
Get-Content 'C:\ProgramData\JenkinsAsService\agent.log' -Tail 50 -Wait
# Check Event Log
Get-EventLog -LogName Application -Source JenkinsAsService -Newest 20The watchdog is event-driven — it awaits the process exit signal, not a polling timer. The supervision loop has two states:
Bring-up (no live agent) — used for both first launch and recovery, so a transient outage at boot doesn't fault the service:
- Wait with exponential backoff (10s, 20s, 40s, ... capped at 300s)
- Test TCP connectivity — an unreachable controller is retried indefinitely and never counts toward
MaxRetries - Refresh
agent.jarvia conditional GET (ETag/304); on a 304 the cached jar is SHA-256-verified (re-downloaded if it fails), then start a new agent process
Live agent — awaits the exit signal or a 60s stability timer:
- On 60s of stability, reset the crash counter
- On a crash (real exit before stability): log the exit code + SEVERE count, kill, and in
Autotransport mode toggle WebSocket ↔ direct-TCP for the next attempt - Count the crash; if consecutive crashes exceed
Recovery:MaxRetries, callStopApplication()so the service stops (rather than leaving a dead agent behind aRUNNINGservice)
The MSI installer configures Windows-level service recovery automatically: first, second, and third failures all restart the service after 10 seconds, with the failure counter resetting daily — so a crash-loop give-up is itself recovered by SCM.
Log messages the service emits, by severity. Errors stop the service (or the current startup); Warnings are non-fatal — the watchdog keeps going.
| Log message / symptom | Type | Meaning & fix |
|---|---|---|
'Connection:Url' is a mandatory field / 'Secret:Value' is a mandatory field |
Error | A required field in appsettings.json is empty — fill it in. |
Connection:Url must include an explicit port |
Error | Write the port explicitly: https://jenkins:8443. A URL with no port is rejected; an explicit standard :443 is accepted. |
Connection:Url is not a valid absolute URL |
Error | Malformed URL — fix the value. |
Cannot find java.exe |
Error | Set Agent:JavaPath to the JDK bin folder, or set JAVA_HOME. |
DPAPI decryption failed / TPM decryption failed / env var / credential not found |
Error | The secret can't be decrypted or read on this machine — re-run update-secret (DPAPI/TPM keys are machine-bound and don't move between hosts). |
Service failed — shutting down |
Error | A non-transient failure (bad config, unresolvable secret, missing Java) — check the logged exception; the service stops so SCM recovery can restart it. |
Watchdog: Failed to download or start the agent — will retry |
Error | Jar download or process launch failed — check the jar URL, disk permissions, and Java. Retried automatically with backoff. |
Watchdog: Max retries (N) exceeded — the agent keeps crashing. Stopping the service. |
Error | A persistent crash-loop hit Recovery:MaxRetries; the service stops itself so Windows Service Recovery restarts it. Enable DebugMode and inspect the exit codes. |
Connection:Url uses http — agent secret will be sent unencrypted. Consider HTTPS. |
Warning | Plain HTTP sends the secret in cleartext. Use HTTPS. (Not blocked.) |
Connectivity test to <host>:<port> timed out / failed |
Warning | Controller unreachable — check firewall, DNS, the port, and that Jenkins is running. |
Watchdog: Jenkins unreachable — will keep retrying. |
Warning | Controller down during (re)connect. Retried indefinitely with backoff; does not count toward MaxRetries. |
Watchdog: Agent emitted N SEVERE event(s) before exiting. |
Warning | The agent logged SEVERE lines before dying — check the controller logs and that AgentName/Secret match the node exactly. |
Watchdog: <transport> transport failed to stabilise — falling back to <other>. |
Warning | Connection:Method = Auto toggled WebSocket ↔ direct-TCP inbound. If it flip-flops, make sure the intended transport is enabled on the controller. |
Could not restrict ACL on agent secret file |
Warning | The defense-in-depth ACL couldn't be applied; the secret file is still usable and already lives in a restricted folder — check the data-folder permissions. |
| Agent connects then disconnects randomly | Warning | Connection:AgentName and Secret:Value must match the Jenkins node config exactly; check network stability. |
dotnet restore -p:RestoreLockedMode=true
dotnet test -c Release
dotnet publish src/JenkinsAsService `
--nologo -c Release `
-p:RestoreLockedMode=true `
-r win-x64 --self-contained true `
-o publish/x64/ `
-p:PublishSingleFile=true `
-p:EnableCompressionInSingleFile=true `
-p:PublishReadyToRun=true
# Optional: build WiX v5 MSI
dotnet build src/JenkinsAsService.Installer -c Release `
-p:RestoreLockedMode=true -p:PublishDir=../../publish/x64/ -p:Version=1.0.0- TLS 1.2+ enforced by default (.NET 10), with optional controller certificate pinning (
ControllerCertThumbprint) - Secrets encrypted at rest (TPM 2.0 hardware-backed key, DPAPI machine/user scope, or CredMgr), redacted from all logs, and passed to the agent off the command line via
-secret @<file>so they never appear in the process table - Least-privilege virtual service account, deny-by-default environment block for the agent child, and Win32 process-mitigation policies (no remote/low-IL/non-System32 DLL loads, extension-point injection disabled)
- Binary/data separation: read-only binaries in
Program Files, writable runtime data inProgramData— a malicious pipeline can't overwrite the service.exe. The cachedagent.jar(+ SHA-256) lives in anagent\subfolder isolated from the buildwork\dir and is integrity-checked before each launch, so a build step can't swap the binary the watchdog runs - Deterministic builds with locked NuGet restore and embedded PDB symbols
- CycloneDX SBOM generated in CI and attached to every release (with SHA-256 checksum)
- Unit tests run on every push and PR
- 6 security scans: CodeQL (C# SAST + Actions YAML), Semgrep (pattern SAST + secrets), Gitleaks (git history), PSScriptAnalyzer (PowerShell, via direct
pwshstep), Dependency Review (CVE gate), Trivy (SCA, NVD + GHSA + OSV); all actions SHA-pinned against tag mutation - Automated dual-arch release pipeline with SHA256 checksums
See Security Policy for vulnerability reporting.
JenkinsAsService is local-first and collects nothing about you: the service runs entirely on your machine, telemetry is opt-in and off by default, and there is no update check or "phone home." See the Privacy Policy for what the service and the docs site do and don't collect.