搞 Windows 运维这些年,我最大的体会是:很多人对服务的理解停留在"任务管理器里有个进程,开机自动跑起来"这个层面,真遇到服务起不来、权限报错、账户登录失败的时候,就开始瞎试。尤其是"本地系统""网络服务""本地服务"这三个内置账户,名字长得像,实际权限差得十万八千里,一旦搞混,后面全是坑。这篇东西我不打算写成官方文档,就按我平时排查问题的思路,把 Windows 服务、三个内置账户、以及 PowerShell 管服务的常用命令一次讲透,想系统了解服务机制的人、经常被服务问题折磨的运维朋友,应该都能用得上。
1. 三种内置账户的前世今生:为什么要分清本地系统、网络服务和本地服务
1.1 三个账户在权限模型中的定位差异
很多初学者会问:同样是 Windows 自带的账户,为什么还要分成三种?直接都用 Administrator 跑服务不就行了?
这个问题问到点子上了。Windows 服务本质上是一个"没有人登录也能运行"的特殊进程,它的身份不是某个坐在屏幕前敲键盘的用户,而是一个"安全主体"(Security Principal)。系统里每个进程都带着一个访问令牌(Access Token),令牌里写着这个进程能以谁的身份、访问哪些资源、拥有哪些权限。服务也不例外,它启动时会把指定的账户信息装进令牌里。
三个内置账户的区别,主要就在这个令牌的"含金量"上:
本地系统账户(Local System,实际名字是
NT AUTHORITY\SYSTEM):这是 Windows 里权限最高的内置账户,比管理员组的 Administrator 权限还大。它几乎可以访问整个系统,包括注册表、文件系统、其他进程的内存空间。它不受普通 ACL(访问控制列表)的限制,而且能直接以"计算机身份"向外发起网络请求。在域环境里,这个"计算机身份"相当于域内一台普通计算机,可以被域内资源识别。网络服务账户(Network Service,
NT AUTHORITY\NETWORK SERVICE):本地权限比 Local System 低不少,但它在访问网络资源时,会自动使用"计算机账户"的凭据。也就是说,如果一台机器加入了域,用 Network Service 跑的服务访问另一台域内服务器的共享文件夹时,对方看到的是这台机器(计算机账户),而不是匿名的空会话。这是很多企业内网应用选择它跑中间层服务的关键原因。本地服务账户(Local Service,
NT AUTHORITY\LOCAL SERVICE):三个内置账户里权限最小的一个。它在网络访问时用的是"匿名令牌"(NULL Session),对方看到的既不是用户名也不是计算机名,而是"未认证的匿名用户",所以默认情况下用它访问其他机器的共享资源几乎都会被拒。但如果某个服务只需要跑在本地,不需要联网,那它就是最安全的选择。
用一个生活化的类比:Local System 是公司的"万能门禁卡",全楼都能刷,包括机房和财务室;Network Service 是一张印着你公司名字的"工牌",去别的公司谈事,人家认你的单位抬头;Local Service 则是一张不写名字的"临时访客卡",只能在公司一楼大厅转转,想去哪都难。
1.2 服务如何使用这些账户:令牌构造、网络凭据与安全上下文
服务控制管理器(SCM,Service Control Manager)在启动服务时,会根据服务配置里"登录为"那一栏的账户信息,调用 LSA(本地安全机构)来创建这个进程的访问令牌。这里面有一个非常容易忽略的细节:SCM 要求启动服务的账户必须拥有"作为服务登录"(SeServiceLogonRight)的用户权利。如果这个权利缺失,服务会直接报错 1069(由于登录失败而未能启动服务)。
三个内置账户默认都已经被赋予了"作为服务登录"这个权利,所以大家平时用 Local System 跑服务从没遇到过这个问题。但一旦你把服务改成自定义账户,比如某个本地普通用户,就必须先去本地安全策略里把这个用户加进"作为服务登录",否则服务怎么都起不来。
而在网络访问这个维度上,三者的区别非常微妙。Local System 和 Network Service 在需要访问网络资源时,都会走"计算机账户"凭据。在工作组环境里,这个凭据就是机器名加一串随机密码(由系统自动维护);在域环境里,就是域名加上机器账户的密码(同样由系统自动轮换)。所以你会发现,用 Local System 跑一个需要访问局域网共享的备份任务,明明是脚本里的路径写的是\\server\share,机器也没输入过任何用户名密码,居然能成功——靠的并不是什么魔法,而是这个"计算机身份"自动认证机制。
不过这里有个巨坑:如果目标服务器开启了"仅来宾"模式,或者禁用了"经典"共享权限模型,那么计算机账户的认证方式可能会退化成 Guest 匿名访问,导致明明有权限却报 401、找不到网络路径,这类问题排查起来相当折磨人。我后面会专门讲排查案例。
2. PowerShell 服务管理指令实战:从查询到启停的日常操作
PowerShell 管服务,其实核心就这么几个命令:Get-Service、Start-Service、Stop-Service、Restart-Service、Set-Service,以及从 CIM 层面下手的Get-CimInstance Win32_Service。但光知道命令名用处不大,关键是搞清楚它们各自的边界。
2.1 Get-Service 的扩展用法:状态过滤、名称匹配与自动化判断
最常见的用法是Get-Service,不加参数默认列出所有服务。但实际工作中没人愿意盯着满屏的服务名去找,我更常用的几个姿势是:
# 只看正在运行的服务 Get-Service | Where-Object { $_.Status -eq 'Running' } # 按名字模糊查找,比如带 sql 关键词的服务 Get-Service -Name '*sql*' # 按显示名查找 Get-Service -DisplayName '*windows update*' # 统计各种状态的数量 Get-Service | Group-Object Status有一次排查性能问题,我需要快速找出哪些服务是"自动启动但当前没跑起来"的异常状态,直接一条命令搞定:
Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }这条命令的思路是:StartType 表示启动类型(Automatic、Manual、Disabled),Status 表示当前运行状态。一个服务如果是自动启动却不在运行,要么是启动失败,要么是被手动停掉,无论如何都值得查一下原因。
这里补充一个小白很容易踩的坑:服务名(Name)和显示名(DisplayName)不是一回事。以 Windows Update 服务为例,它的服务名是wuauserv,显示名是"Windows Update"。用Get-Service -Name "Windows Update"会报错,必须用Get-Service -DisplayName "Windows Update"或者直接用Get-Service wuauserv。所以你在写脚本的时候,最好先用Get-Service | Select-Object Name,DisplayName看一眼实际的服务名,别凭记忆写。
2.2 启停服务与设置启动类型的命令组合:Restart-Service、Set-Service、sc.exe 的取舍
操作类命令的使用场景比较清晰:启动用Start-Service,停止用Stop-Service,重启用Restart-Service。但有几个细节值得说:
第一,Restart-Service 不是"先 Stop 再 Start"那么简单。它对服务是多了一步"等待服务进入停止状态"的处理。有些服务在停止时会做清理工作,比如把缓冲区数据落盘,如果你先执行 Stop 再立刻 Start,可能因为服务还没完全停止而报错。而Restart-Service内部会等待服务状态变为 Stopped,虽然默认有一个超时时间,但整体上比手动两条命令稳妥。
第二,Set-Service 可以改启动类型,但它能改的字段有限。比如:
# 把服务改成手动启动 Set-Service -Name 'wuauserv' -StartupType Manual # 把服务改成禁用 Set-Service -Name 'wuauserv' -StartupType Disabled这个命令对绝大多数场景足够。但如果你要改的是"服务登录账户"(LocalSystem / NetworkService / LocalService / 指定账户),那就不是Set-Service能搞定的了,得靠sc.exe config命令,或者直接改注册表里的ImagePath、ObjectName等键值。这部分我下一章详细展开。
第三,sc.exe是老牌的 CMD 工具,但 PowerShell 里照样能用,而且有些场景比原生 Cmdlet 好用。比如查看服务的完整配置:
sc.exe qc wuauserv sc.exe query wuauserv sc.exe config wuauserv start= demand注意:sc.exe config里start=后面必须有一个空格再写值,这个语法坑了我当年很久,它在 CMD 和 PowerShell 里的参数解析规则不太一样。start= auto表示自动,start= demand表示手动,start= disabled表示禁用。
2.3 WMI/CIM 查询在服务运维中的补充价值
如果要查服务的"登录账户"到底是哪个,光靠Get-Service是不行的,因为它的默认输出里根本没有这一项。这时候必须用 CIM:
Get-CimInstance Win32_Service | Select-Object Name, State, StartMode, StartNameWin32_Service的StartName字段会直接告诉你这个服务是以哪个账户身份运行的。比如你会看到:
LocalSystem:表示本地系统账户NT AUTHORITY\NetworkService:表示网络服务NT AUTHORITY\LocalService:表示本地服务.\Admin或域\用户名:表示自定义账户
这个查询在服务大量异常、需要批量检查账户归属时非常有用。有一次我接手一台别人装的服务器,上面十几个服务都跑在 Local System 下,我通过这条命令列出来之后,直接跟负责人对着清单一条条评估哪些能降权,最后大部分都改成了 Network Service 或 Local Service,风险面一下子小了很多。
3. 给服务"换身份":修改服务账户时的权限逻辑与实际坑点
改服务账户这件事,说简单也简单,说麻烦也真的能折腾一天。核心难点不是"命令怎么敲",而是改完之后整个系统里还有一堆"残留权限"概念,它们不会因为你把账户改了就跟过去。
3.1 修改内置账户归属:为什么按"该账户"而不是"登录为"
在Windows 服务属性对话框里,登录选项卡分了两块:一个是"登录身份"下面的"本地系统账户""此账户",另一个是"允许服务与桌面交互"复选框。很多人改服务身份,直接在这一页改,但完全没有意识到,这里改的只是服务配置里的"ObjectName",跟服务进程真正能碰到的文件、注册表项权限是两套体系。
举个例子。你有一个服务,原来跑在 Local System 下,一直往某个目录写日志,目录的 ACL 里可能压根没有其他账户的条目。现在你把服务改成 Network Service,服务确实能启动,但一写日志就报"拒绝访问"。这个问题的根源是:目录权限是跟着 SID(安全标识符)走的,不是跟着服务名走的。Local System 的 SID 是S-1-5-18,Network Service 的 SID 是S-1-5-20,Local Service 的 SID 是S-1-5-19,三者完全不同。切换账户等于换了个身份去访问资源,原身份能碰的,新身份不一定能碰。
所以我的建议是,任何服务改完登录账户之后,不要急着欢呼,先花十分钟做三件事:
- 查一下服务访问的关键目录的 ACL,给新账户显式授权。
- 查一下服务访问的注册表键的权限,必要时用
regedit右键权限给新账户添加读取、写入权限。 - 如果服务有向事件日志写日志的需求,确认新账户是否有权限创建事件源(Event Source)。
3.2 域账户、组托管服务账户与密码轮换的选择
自定义账户跑服务,最大的痛点就是密码过期。Windows 默认的密码策略要求用户密码定期更换,一旦你忘了更新服务里的密码,服务就会在密码过期的瞬间悄然死掉,或者重启后怎么都起不来,报 1069。
这几天我至少收到过三个类似问题,全是"服务突然启动失败,错误代码 1069",最后查出来都是密码过期或密码错误。解决方案有两条路:
- 如果你用的是域账户:把服务配置里的密码改成新密码,或者直接勾选"密码永不过期"。前者是正规操作,后者是临时救火,但很多人图省事选后者,安全审计的时候容易挨骂。
- 更推荐的方案是组托管服务账户(gMSA,Group Managed Service Account)。它的密码由域控自动管理,每30天自动轮换一次,服务不需要知道密码是什么。Windows Server 2012 以后的域环境都能用。如果你的环境支持,哪怕只是两三台服务器,我也建议优先用 gMSA,它能省掉一整个"密码到期杀服务"的坑。
3.3 修改完账户后常见的遗留问题:注册表权限、目录 ACL 与服务隔离
还有一类坑属于"服务隔离"范畴。比如你把某个服务从 Local System 改成 Network Service 后,发现服务能启动,但功能不正常,日志里有类似"访问被拒绝""目录不存在"的报错。除了前面说的目录 ACL 问题,还有可能是注册表项权限的问题。
很多程序的配置存在注册表里,安装服务时注册表项的 ACL 默认只给了 SYSTEM 和管理员,Network Service 读不了。解决办法是用regedit找到对应的项,右键权限,把NT AUTHORITY\NETWORK SERVICE加进去,至少给"读取"权限;如果服务要写配置,再给"完全控制"。
另外要注意,在按"登录身份"修改服务时,桌面上那句"允许服务与桌面交互"很容易被误勾选。现代 Windows 版本里,服务和用户桌面处于不同的会话,哪怕是 Local System 跑的服务,默认也无法直接弹窗给用户看。曾经为了调试一个程序,我勾选了这个选项并切换到 Local System,结果服务确实弹了个窗口在会话 0 里,但用户根本看不到,白白折腾半天。现在除了极少数老程序,我完全建议不勾选这个选项,隔离反而更安全。
4. 服务启动失败排查链路:凭据、权限与账号策略的联动影响
服务起不来,最怕的就是瞎猜。补齐一条完整的排查链路,能帮你省下大把时间。
4.1 一次"服务本机启动失败"的完整排查记录
我有一次给客户排查一个自定义服务,现象是:服务在"服务"面板里显示"正在启动",然后过几秒变成"已停止",事件查看器的系统日志里有一堆来源为Service Control Manager的 7000 事件(服务启动超时)和 7034 事件(服务意外终止)。
我当时的排查顺序是这样的:
- 先看事件日志。事件查看器 -> Windows 日志 -> 系统,按来源过滤 Service Control Manager。7000 事件后面通常会跟着一个系统错误代码,比如 "错误 1068"(依赖服务无法启动)或 "错误 1069"(登录失败)。这个错误代码是整条排查链路的入口。
- 再用 qc 看服务配置。运行
sc.exe qc 服务名,检查 BINARY_PATH_NAME(程序路径)、SERVICE_START_NAME(登录账户)、DEPENDENCIES(依赖关系)。我当时发现这个服务的启动账户写的是一个本地用户.\devuser。 - 确认账户是否存在。用
net user devuser看用户是否存在、密码有没有过期。查完发现,确实有,但密码过期了。 - 验证密码是否和服务配置一致。这里有个笨但有效的办法:在服务属性 -> 登录 -> 此账户,重新输入一遍密码,点确定。如果系统提示"找不到此账户"或"用户名或密码错误",基本就能确认问题。
- 确认"作为服务登录"权限。就算密码正确,如果
devuser没有被授予"作为服务登录"权限,依然会报 1069。用secpol.msc-> 本地策略 -> 用户权限分配,找"作为服务登录",把用户加进去。
这个案例最终是密码过期 + 密码不一致双重问题,改完密码立刻就好了。但整个过程如果跳过事件日志直接去改配置,很容易漏掉真正的根因。
4.2 从事件日志到错误代码的定位方法
事件日志里常见的 Service Control Manager 错误代码,我用一张表整理过,排查的时候对着找非常快:
| 错误代码 | 常见含义 | 通常原因 |
|---|---|---|
| 1068 | 依赖服务无法启动 | 服务依赖的另一个服务没启动或被禁用 |
| 1069 | 登录失败 | 密码错误、密码过期、账户不存在、缺乏登录权限 |
| 1079 | 此服务的账户与其它运行在相同进程中的服务不同 | 多个服务共享同一个进程,但登录账户不一致 |
| 1072 | 已删除指定的服务 | 服务配置文件指向了已不存在的目标 |
| 1053 | 服务没有及时响应启动请求 | 服务程序启动逻辑卡住,超时(默认30秒) |
| 7000 | 服务启动超时 | 服务所在进程启动过慢,或初始化失败 |
| 7031 | 服务意外终止 | 服务运行中崩溃,常伴程序 bug 或内存问题 |
| 7023 | 服务终止并出现错误 | 服务退出时带了一个错误码,需要结合应用日志看 |
表里的 1079 我特别提一下。它很隐蔽,常见于一个"服务宿主进程"里承载多个服务的情况,比如某些第三方软件把自己的几个子服务注册到了同一个 svchost.exe 进程里,你用services.msc修改其中一个子服务的登录账户,导致它和同一进程里其他子服务的账户不一致,整个进程就起不来了。解决办法是把同一组服务全部改成相同账户,或者干脆拆开进程。
4.3 常见启动失败原因对照表
还有一份"现象到原因"的对照思路,适合快速定位:
- 服务启动后马上停止,事件里没有错误:通常是服务自己的逻辑问题,先看应用程序日志,别急着改配置。
- 服务启动时间特别长,然后报 1053:检查服务初始化代码里有没有连接网络、等待数据库这类阻塞操作,必要时候把默认的 30 秒超时改大(注册表
ServicesPipeTimeout)。 - 服务能启动,但访问网络资源失败:先确认服务账户是 Local System 还是 Network Service,再确认目标服务器上计算机账户有没有权限。
- 手动启动服务时报"拒绝访问":打开一个"以管理员身份运行"的 PowerShell 或 CMD 再试。很多人忽略这一点,非管理员权限默认无法启停系统服务。
5. 把服务管起来的配置建议与安全基线
前面聊了这么多"怎么操作",最后这部分我想从治理的角度收个尾。服务账户这块,如果没有一套明确的规范,服务器越多,坑越大。
5.1 给服务账户做最小权限分配的判断方法
我的原则很简单:能跑在 Local Service 下,就不跑 Network Service;能跑 Network Service,就不跑 Local System;能跑普通域账户,就别把 Domain Admin 直接挂上去。
但怎么判断一个服务到底能不能降权?建议按三个问题逐层检查:
- 这个服务是否需要访问外部机器的共享文件夹、数据库或 HTTP API?如果不需要,优先考虑 Local Service。
- 是否需要以计算机身份被其他机器识别?如果需要,用 Local System 或 Network Service。Local System 权限过大,优先试 Network Service,比如典型的 Web 应用池默认用的就是 Network Service。
- 是否存在必须管理员权限才能操作的本地动作?比如写系统目录、打开受保护端口、操作其他进程。如果确实有,再考虑 Local System 或专门的本地管理员账户。
实际工作中,很多服务用 Local System 纯粹是安装时图省事。你花点时间逐个评估,把没必要高权限的降下来,攻击者一旦通过这些服务发起的横向攻击空间会小非常多。
5.2 授权"作为服务登录"的正确操作路径
给一个用户授予服务登录权限,最稳妥的方式是:
- 运行
secpol.msc打开本地安全策略。 - 左侧展开"本地策略" -> "用户权限分配"。
- 右侧找到"作为服务登录"(英文是
Log on as a service),双击打开。 - "添加用户或组",输入要授权的账户,比如
devuser,确定。
在域环境里,如果服务跑在域账户下,还需要在域策略里配置"作为服务登录",或者使用 gMSA(gMSA 会自动处理机器接入时的权限分配)。这里有个容易懵的点:域账户的"作为服务登录"如果只在本地策略里加了,理论上也可以,但域策略刷新时可能被覆盖,所以要么配置在域策略里,要么用 gMSA 绕开这个难题。
5.3 我这边踩过的坑与固化下来的日常检查脚本
最后分享一个我建议每周至少跑一次的检查脚本逻辑,用来提前发现问题:
# 找出启动类型为自动但当前不在运行的服务,同时对停止的服务输出账户信息 Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' } | ForEach-Object { $svc = Get-CimInstance Win32_Service -Filter "Name='$($_.Name)'" [PSCustomObject]@{ Name = $svc.Name DisplayName = $svc.DisplayName State = $svc.State StartMode = $svc.StartMode StartName = $svc.StartName } } | Format-Table -AutoSize这个脚本输出哪些自动启动服务没跑起来,以及它们以什么账户运行。自动启动却停了,要么是上次注销或重启后启动失败,要么是有人手动停掉。看到结果后再去事件日志里核对,往往能在大规模故障爆发之前发现问题。
我还整理过一条检查服务账户密码过期的命令,适合非域账户时用:
# 查看指定用户的密码过期时间 net user devuser /domain如果是域账户,直接看域控上用户的属性;如果是 gMSA,完全不用担心密码过期,这也是我强烈推荐它的原因。
日常维护 Windows 服务,我个人的体会是:把三个内置账户的权限边界吃透,比记住一百条命令更重要。权限模型清楚了,命令只是实现思路的工具。像"服务为什么突然起不来"这种问题,大多数时候不是命令不熟,而是没找准账户、权限、密码这条三角关系里到底哪一环变了。按本文这套思路,先查事件日志,再对错误代码,接着核对账户和密码,最后确认登录权限和路径 ACL,基本能把九成以上的服务启动问题压下去。剩下的那些启动即崩溃的,就老实看应用日志,那又是另一门功课了。