我在真内核上把安装序列完整走了一遍(容器里补了 useradd/systemctl 的替身,其余全是真的), 暴露出一个只有真跑才看得见的缺陷: 装完 → `install` 启动服务 → 但那时**还没有配对的手机**,而守护进程**故意**拒绝空的设备列表 (一个什么都不能授权的 agent 不该坐在那里显得健康)。于是 `Restart=always` + `RestartSec=5` 变成每 5 秒重启一次,永远,journal 里刷同一行。实测:exit 1,"devices is empty, so no phone could ever be authorised"。 配对发生在安装**之后**(运维在节点上跑 `--pair`,然后在手机上配对),所以这不是异常状态, 而是流程的必经一步。修法两条: - 单元加 `StartLimitIntervalSec=300` / `StartLimitBurst=10`:十次之后 systemd 放弃,单元进入 `failed`——一个运维看得见、能处理的状态,而不是无限重启。 - `install` 的结束语改成说清这一步:服务会退出并停住,下一步是 `--pair`,用 `systemctl status` 看状态。此前它说"installed and running",而那是假的。 同一轮演练确认了此前所有修复在真内核上成立:服务用户建了、配置与日志目录交给它了、配置对 该用户可读、单元来自二进制且内容一致、卸载按序拆干净(用户/目录/单元全无残留)。 Go 全包通过(新增一项守着"单元必须有启动上限,且 install 必须说出下一步")。
76 lines
3.0 KiB
Desktop File
76 lines
3.0 KiB
Desktop File
[Unit]
|
|
Description=MultiSimAdmin relay agent
|
|
Documentation=https://gitea.chickliu.fun/Hermes/msa-agent
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/opt/msa-agent/msa-agent --config /etc/msa-agent/config.json
|
|
Restart=always
|
|
# Backing off on our own is not enough: a relay that is down for an hour should not be hammered by
|
|
# every node in the fleet the moment it comes back.
|
|
RestartSec=5
|
|
# A freshly installed node has no paired devices, and the daemon refuses to run in that state on
|
|
# purpose: an agent that could authorise nothing must not sit there looking healthy. But pairing happens
|
|
# *after* install — the operator runs `--pair` on the node and then pairs the phone — so between those
|
|
# two moments this unit would restart every five seconds, forever, filling the journal with the same
|
|
# line. Measured on a real kernel: exit 1, "devices is empty, so no phone could ever be authorised".
|
|
#
|
|
# StartLimitIntervalSec/StartLimitBurst make systemd give up instead: ten attempts inside five minutes
|
|
# and the unit enters `failed`, which is a state an operator can see and act on. The install command
|
|
# prints the next step precisely so that state is expected rather than surprising.
|
|
StartLimitIntervalSec=300
|
|
StartLimitBurst=10
|
|
TimeoutStopSec=20
|
|
KillSignal=SIGTERM
|
|
|
|
User=msa-agent
|
|
Group=msa-agent
|
|
|
|
# The agent needs almost nothing from the host. Everything below is a capability the process never
|
|
# uses, removed so that a compromise of the agent is not automatically a compromise of the box.
|
|
NoNewPrivileges=yes
|
|
PrivateDevices=yes
|
|
PrivateTmp=yes
|
|
PrivateUsers=yes
|
|
ProtectSystem=strict
|
|
ProtectHome=yes
|
|
ProtectKernelTunables=yes
|
|
ProtectKernelModules=yes
|
|
ProtectClock=yes
|
|
ProtectControlGroups=yes
|
|
ProtectHostname=yes
|
|
ProtectKernelLogs=yes
|
|
RestrictNamespaces=yes
|
|
RestrictRealtime=yes
|
|
RestrictSUIDSGID=yes
|
|
LockPersonality=yes
|
|
MemoryDenyWriteExecute=yes
|
|
RemoveIPC=yes
|
|
UMask=0077
|
|
|
|
# Configuration and the audit log are the only writable paths. ReadWritePaths is what keeps
|
|
# ProtectSystem=strict from making the audit log unwritable.
|
|
ReadWritePaths=/var/log/msa-agent
|
|
ConfigurationDirectory=msa-agent
|
|
LogsDirectory=msa-agent
|
|
|
|
# AF_INET and AF_INET6 only: the agent dials out to the relay and to 127.0.0.1, so it has no reason
|
|
# to reach a Unix socket, a netlink family or a packet socket.
|
|
#
|
|
# No IPAddressDeny / IPAddressAllow pair here, deliberately. It reads like the obvious way to pin the
|
|
# agent to loopback, and it is wrong: those directives filter both directions, so IPAddressDeny=any
|
|
# would also cut the outbound connection to the relay the agent exists to maintain. The loopback
|
|
# invariant is enforced in code by internal/egress, which is testable, rather than by a directive
|
|
# that silently breaks the only path out.
|
|
RestrictAddressFamilies=AF_INET AF_INET6
|
|
# No capability at all, so the process cannot even ask to bind a privileged port.
|
|
CapabilityBoundingSet=
|
|
AmbientCapabilities=
|
|
SystemCallFilter=@system-service
|
|
SystemCallErrorNumber=EPERM
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|