installer: add SERVICE_ACCOUNT and document a least-privilege service account (#2769)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 21:49:21 +02:00
committed by GitHub
parent dfc5653376
commit be5942d943
6 changed files with 242 additions and 24 deletions
+9
View File
@@ -112,6 +112,8 @@ Each release provides a .msi installer. The installer will setup the windows_exp
If the installer is run without any parameters, the exporter will run with default settings for enabled collectors, ports, etc.
The service runs as `LocalSystem` unless `SERVICE_ACCOUNT` is set. To run it as an unprivileged service account instead, see [Running as a least-privilege service account](docs/permissions.md).
The installer provides a configuration file to customize the exporter.
The configuration file
@@ -134,6 +136,8 @@ The following parameters are available:
| `ADDLOCAL` | Enables features within the windows_exporter installer. Supported values: `FirewallException` |
| `REMOVE` | Disables features within the windows_exporter installer. Supported values: `FirewallException` |
| `APPLICATIONFOLDER` | Directory to install windows_exporter. Defaults to `C:\Program Files\windows_exporter` |
| `SERVICE_ACCOUNT` | Account the service runs as, for example `NT SERVICE\windows_exporter`. Defaults to an empty string (`LocalSystem`). The installer does not grant the account any rights, see [permissions](docs/permissions.md) |
| `SERVICE_PASSWORD` | Password of `SERVICE_ACCOUNT`. Only a user account needs one; a virtual account or a gMSA has none. The installer does not write it to its log |
> [!NOTE]
> The installer properties are always preferred over the values defined in the config file. If you prefer to configure via the config file, avoid using any of the properties listed above.
@@ -157,6 +161,11 @@ Define a config file.
msiexec /i <path-to-msi-file> --% CONFIG_FILE="D:\config.yaml"
```
Run the service as its virtual account. Grant the account its rights afterwards, see [permissions](docs/permissions.md).
```powershell
msiexec /i <path-to-msi-file> --% SERVICE_ACCOUNT="NT SERVICE\windows_exporter"
```
Alternative install directory
```powershell
msiexec /i <path-to-msi-file> --% ADDLOCAL=FirewallException APPLICATIONFOLDER="F:\Program Files\windows_exporter"
+4
View File
@@ -6,6 +6,10 @@ the [Windows exporter mixin](../contrib/mixin/README.md). Preview the runbook si
from the repository root with `hugo server --source docs`; its dedicated GitHub
Actions workflow publishes default-branch changes to GitHub Pages.
[Running as a least-privilege service account](permissions.md) describes
the rights the exporter needs and which collectors lose data without
administrator rights.
# Collectors
- [`ad`](collector.ad.md)
- [`adcs`](collector.adcs.md)
+176
View File
@@ -0,0 +1,176 @@
# Running as a least-privilege service account
By default, the MSI installs the `windows_exporter` service as `LocalSystem`.
Every collector works with that account. The `SERVICE_ACCOUNT` property of the
MSI sets another account, but the MSI does not grant that account any rights.
The exporter only reads data and needs no administrator rights for the common
collectors. This page describes how to run the service as an unprivileged
account instead, and which collectors then lose data.
## Choose an account
- A virtual service account, `NT SERVICE\windows_exporter`. Windows creates and
manages it with the service, and it has no password. It gets the "Log on as
a service" right through `NT SERVICE\ALL SERVICES`, which Windows includes in
that right by default. A group policy that sets "Log on as a service" without
`NT SERVICE\ALL SERVICES` stops virtual accounts from starting. This is the
simplest choice on a single host.
- A group managed service account (gMSA), for example `CONTOSO\svc-exporter$`,
when the account needs rights in the domain or the same identity on many
hosts. Install it on the host with `Install-ADServiceAccount` first. It needs
the "Log on as a service" user right (`SeServiceLogonRight`).
- A domain or local user. It needs the "Log on as a service" user right
(`SeServiceLogonRight`) and a password that you have to rotate.
On top of the account itself, add it to the local **Performance Monitor Users**
group. Windows uses this group to grant access to performance counter data
(PDH), which most collectors read. Users who run the exporter like this in
[#594](https://github.com/prometheus-community/windows_exporter/issues/594)
report that the group is enough for the default collectors.
**Performance Log Users** is not needed: it adds the right to create data
collector sets and logs, and the exporter only reads counters.
## Set up the service
Install the MSI with the account:
```powershell
msiexec /i <path-to-msi-file> --% SERVICE_ACCOUNT="NT SERVICE\windows_exporter"
```
For a gMSA, set `SERVICE_ACCOUNT="CONTOSO\svc-exporter$"` (not verified through
the MSI). For a user, use `CONTOSO\user` or `.\user` for a local user and also
set `SERVICE_PASSWORD`; the MSI keeps it out of its log. A gMSA or a user needs
"Log on as a service" before you install, because the MSI starts the service
right away. The MSI starts it before the account has the other rights below,
so restart the service after granting them.
Then run this in an elevated PowerShell. Windows resolves the virtual account
`NT SERVICE\windows_exporter` only once the service exists, so run it after the
installation.
```powershell
$service = 'windows_exporter'
$account = "NT SERVICE\$service"
# Only needed if the MSI was installed without SERVICE_ACCOUNT. A virtual
# account has no password.
sc.exe config $service obj= $account
# Grant read access to performance counters. S-1-5-32-558 is the Performance
# Monitor Users group in every display language. SilentlyContinue lets the script
# run again after an MSI upgrade, when the account is already a member.
Add-LocalGroupMember -SID 'S-1-5-32-558' -Member $account -ErrorAction SilentlyContinue
# The default textfile directory and config file below Program Files are readable
# by all users. Grant read access to other locations you use, for example:
# icacls 'D:\metrics' /grant "${account}:(OI)(CI)RX"
# icacls 'D:\windows_exporter\config.yaml' /grant "${account}:R"
Restart-Service $service
```
`Add-LocalGroupMember` and `Remove-LocalGroupMember` come from the
`Microsoft.PowerShell.LocalAccounts` module, which needs 64-bit PowerShell.
Without `SERVICE_ACCOUNT`, use `sc.exe config windows_exporter obj= 'CONTOSO\svc-exporter$'`
for a gMSA, and also add `password= '<password>'` for a user. For both, grant
"Log on as a service" with `secpol.msc` or a group policy: neither the MSI nor
`sc.exe` grants it, only the Services console (`services.msc`) does when you set
the account there.
The account also needs:
- Read access to the config file (`--config.file`), the textfile directories
(`--collector.textfile.directories`), the files and registry keys that the
`file` and `registry` collectors read, and the TLS key and certificate of
`--web.config.file`.
- Write access to the log file and its directory if `--log.file` is a path. The
default for the service, the Windows event log, needs no extra rights.
Writers of textfile metrics need write access to their directory; the exporter
itself only reads it.
To check the result, look at the account and at the collectors that fail:
```powershell
(Get-CimInstance Win32_Service -Filter "Name='windows_exporter'").StartName
(Invoke-WebRequest -Uri 'http://localhost:9182/metrics' -UseBasicParsing).Content -split "`n" |
Select-String 'windows_exporter_collector_success'
```
How missing rights show up depends on when the collector reads the data:
- Some collectors query WMI while they start: `dns` (`wmi_stats`), `fsrmquota`
and `storage_spaces`. Access denied there is not an expected startup error,
so the exporter exits and the service does not start. The error is in the
Application event log (source `windows_exporter`). Grant access or disable
the collector.
- A collector that fails while scraping has
`windows_exporter_collector_success` 0 and logs the error.
- Partial data loss, such as skipped services, missing scheduled tasks or empty
process labels, leaves `windows_exporter_collector_success` at 1.
### Reverting and MSI upgrades
To run the service as `LocalSystem` again:
```powershell
sc.exe config windows_exporter obj= LocalSystem
Remove-LocalGroupMember -SID 'S-1-5-32-558' -Member 'NT SERVICE\windows_exporter'
Restart-Service windows_exporter
```
The MSI does not remember `SERVICE_ACCOUNT`, like its other properties. An
upgrade installs the service again, as `LocalSystem` unless you pass
`SERVICE_ACCOUNT` (and `SERVICE_PASSWORD`) again. Pass the same properties to
every `msiexec` run. Group memberships and user rights of the account stay.
## Collectors with limits
Collectors that are not listed here read performance counters, the registry
below `HKLM\SOFTWARE`, or other APIs that all users can read, and are expected
to work with the setup above. Only some of them were reported working in the
issue linked above (`cpu`, `logical_disk`, `memory`, `mssql`, `os`, `process`,
`system`); the others are not verified. A counter set whose provider restricts access itself
can still fail.
| Collector | Without administrator rights | What to grant |
|-----------|------------------------------|---------------|
| `container` | The `hcs` sub-collector uses the Host Compute Service, the Host Compute Network service for network statistics and the CRI endpoint (containerd named pipe); memory statistics open the container processes. The `hostprocess` sub-collector also uses the CRI endpoint and opens the job objects of HostProcess containers. Not verified, expected to fail. | Not verified. Use `LocalSystem`. |
| `dhcp` | `server_metrics` reads counters and works. `scope_metrics` uses the DHCP Server API (`dhcpsapi.dll`), which only allows Administrators, DHCP Administrators and DHCP Users. | Add the account to the local **DHCP Users** group (read-only access to the DHCP server). Not verified live. |
| `dns` | `metrics` reads counters and works. `wmi_stats` queries `MicrosoftDNS_Statistic` in `root\MicrosoftDNS`. | WMI namespace security on `root\MicrosoftDNS`, see below. Whether the DNS provider also requires DnsAdmins is not verified. |
| `fsrmquota` | Queries `MSFT_FSRMQuota` in `root\Microsoft\Windows\FSRM`. | WMI namespace security on that namespace. Whether the FSRM provider allows non-administrators is not verified. |
| `hyperv` | The counter sub-collectors work. `host` uses the Host Compute Service; `replica_vm`, `virtual_hard_disk` and `wmi_health` query `root\virtualization\v2`, and `virtual_hard_disk` also opens the virtual disk files. | Membership in **Hyper-V Administrators** gives access to the Hyper-V WMI provider, but it also grants full control over Hyper-V. Read access to the VHD/VHDX files for `virtual_hard_disk`. Not verified; disable these sub-collectors with `--collector.hyperv.enabled` instead. |
| `mscluster` | Reads through the Failover Cluster API (ClusAPI), which requires cluster access. | `Grant-ClusterAccess -User <account> -ReadOnly` on the cluster. Not verified for a virtual account; use a gMSA or domain account, which is the same identity on every node. |
| `process` | All metrics work. The `owner`, `cmdline` and `process_group_id` labels of `windows_process_info` are empty for processes of other users and protected processes (see [`collector.process.md`](collector.process.md)). With `--collector.process.iis`, the application pool comes from the `w3wp` command line, which needs access to the worker process; the fallback queries `root\WebAdministration`. | No grant that opens processes of other users is narrower than administrator rights. WMI namespace security on `root\WebAdministration` for the IIS fallback (not verified). |
| `scheduled_task` | Lists only the tasks whose security descriptor lets the account read them, so tasks are missing. | Not verified. Use `LocalSystem` to see all tasks. |
| `service` | Works for most services. A service whose security descriptor denies the account `SERVICE_QUERY_CONFIG` is skipped with a warning and has no metrics at all, including `windows_service_state`. `windows_service_process` is missing for services whose process the account can't open with `PROCESS_QUERY_LIMITED_INFORMATION`. | Not verified which services are affected. |
| `storage_spaces` | Queries `MSFT_StoragePool` and `MSFT_VirtualDisk` in `root\Microsoft\Windows\Storage`. | WMI namespace security on that namespace. Whether the storage provider allows non-administrators is not verified. |
| `wmi` | Queries the namespaces that you configure. `root\cimv2` allows all authenticated users. | WMI namespace security for other namespaces. |
| `file`, `registry`, `textfile` | Read the paths and registry keys that you configure. | Read access on those files, directories and keys. |
| `update` | Uses the Windows Update Agent API. Not verified. | Not verified. |
The `logical_disk` collector, including `bitlocker_status`, works for
non-elevated accounts (see [`collector.logical_disk.md`](collector.logical_disk.md)).
`cpu_info` and `diskdrive` query `root\cimv2`, which all authenticated users can
read.
### Granting WMI namespace access
By default, all authenticated users have the **Enable Account** permission on
`root\cimv2`. Other namespaces, such as the ones of the DNS, FSRM, storage and
Hyper-V providers, can be limited to administrators. To grant access:
1. Run `wmimgmt.msc`, right-click **WMI Control (Local)** and select
**Properties**.
2. On the **Security** tab, select the namespace, for example
`Root\MicrosoftDNS`, and select **Security**.
3. Add the account (`NT SERVICE\windows_exporter`) and allow **Enable Account**.
The exporter queries locally, so **Remote Enable** is not needed.
4. Restart the service.
A provider can check the caller again after the namespace check and still deny
the query. If the collector keeps failing with access denied, that provider
needs administrator rights or a role-specific group.
+22 -20
View File
@@ -1,22 +1,22 @@
<!--
~ SPDX-License-Identifier: Apache-2.0
~
~ Copyright The Prometheus Authors
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
~ SPDX-License-Identifier: Apache-2.0
~
~ Copyright The Prometheus Authors
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<Wix xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
xmlns="http://wixtoolset.org/schemas/v4/wxs">
xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<DirectoryRef Id="APPLICATIONFOLDER">
<Component Transitive="yes">
@@ -24,10 +24,10 @@
<!-- The "Name" field must match the argument to eventlog.Open() -->
<util:EventSource Log="Application" Name="windows_exporter"
EventMessageFile="%SystemRoot%\System32\EventCreate.exe"
SupportsErrors="yes"
SupportsInformationals="yes"
SupportsWarnings="yes"/>
EventMessageFile="%SystemRoot%\System32\EventCreate.exe"
SupportsErrors="yes"
SupportsInformationals="yes"
SupportsWarnings="yes"/>
<ServiceInstall
Id="InstallExporterService"
@@ -39,6 +39,8 @@
Type="ownProcess"
Interactive="no"
Vital="yes"
Account="[SERVICE_ACCOUNT]"
Password="[SERVICE_PASSWORD]"
Arguments="[ConfigFileFlag] [CollectorsFlag] [ListenFlag] [MetricsPathFlag] [TextfileDirsFlag] [ExtraFlags]">
<ServiceConfig
DelayedAutoStart="yes"
+6
View File
@@ -71,6 +71,12 @@
<Property Id="TEXTFILE_DIRS" Secure="yes" />
<SetProperty Id="TextfileDirsFlag" After="InstallFiles" Sequence="execute" Value="--collector.textfile.directories=&quot;[TEXTFILE_DIRS]&quot;" Condition="TEXTFILE_DIRS" />
<!-- Account of the service, used by ServiceInstall in files.wxs. Empty installs the service as LocalSystem.
The installer does not grant the account any rights, see docs/permissions.md. -->
<Property Id="SERVICE_ACCOUNT" Secure="yes" />
<!-- Only a user account needs a password. Hidden keeps it out of the installer log. -->
<Property Id="SERVICE_PASSWORD" Secure="yes" Hidden="yes" />
<!-- Configuration for how the installer shows in Add/Remove Programs. -->
<Icon Id="icon.ico" SourceFile=".\icon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
+25 -4
View File
@@ -1,8 +1,9 @@
# Tests the MSI lifecycle on a disposable Windows host: upgrade from the
# previous release, repair, uninstall and a fresh install. An independent
# windows_exporter.exe from another directory must survive every operation
# of the new package. Run it only on a throwaway machine such as a CI runner:
# it installs and removes the windows_exporter service.
# previous release, repair, uninstall, a fresh install and an install with a
# virtual service account. An independent windows_exporter.exe from another
# directory must survive every operation of the new package. Run it only on
# a throwaway machine such as a CI runner: it installs and removes the
# windows_exporter service.
[CmdletBinding()]
Param (
@@ -272,6 +273,26 @@ try {
Assert-ForeignAlive -Operation 'uninstall of the fresh install'
Write-Host '::endgroup::'
Write-Step 'Install this build with a virtual service account'
# The installer only sets the account. Without the Performance Monitor
# Users group, collectors may lose data, but the service must start.
$account = "NT SERVICE\$serviceName"
Invoke-Msiexec -Name 'install-account' -Arguments @('/i', "`"$Msi`"", "SERVICE_ACCOUNT=`"$account`"")
Assert-ServiceRunning | Out-Null
$startName = (Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'").StartName
if ($startName -ne $account) {
throw "service $serviceName runs as $startName, expected $account"
}
Assert-ForeignAlive -Operation 'install with a service account'
Write-Host '::endgroup::'
Write-Step 'Uninstall the install with a virtual service account'
Invoke-Msiexec -Name 'uninstall-account' -Arguments @('/x', "`"$Msi`"")
Assert-ServiceRemoved
Assert-ForeignAlive -Operation 'uninstall of the install with a service account'
Write-Host '::endgroup::'
Write-Host 'installer test passed'
} finally {
if (Test-ForeignAlive) {