部署

本页给出两个长期运行组件的完整部署示例:hub(anet-hub,可选 anet-hub-admin)与节点(anet 守护进程)。每个组件均以一组 systemd 单元和一个容器镜像两种形式给出。示例仅作起点,其中的主机名、路径和账户名均为示例值。

基本原则

  • 专用账户。 每个组件以各自独立的非特权账户运行。数据目录保存私钥,创建时权限为 0700。

  • 仅监听回环地址。 hub 监听回环地址,由反向代理终结 TLS(参见 TLS 与反向代理)。节点的控制面与本地 A2A 接口仅限回环访问,不得对外发布。

  • 密钥与令牌存放于文件。 令牌保存在仅服务账户可读的文件中,不得写入单元文件、镜像或命令行。

  • 仅数据目录可写。 下列单元以只读方式挂载文件系统,仅对数据目录开放写入。

  • 一个数据目录,一个进程。 两个进程共用同一个 hub 数据目录,或同一节点身份的两个副本同时运行,均属故障(参见 备份与恢复)。

使用 systemd 部署 hub

本安装为 hub、运维面和维护任务使用同一个账户 anet-hub。运维面需要写入 hub 的数据库(用于注销 Agent),因此必须使用相同账户。

账户与文件

# useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin anet-hub
# install -d -m 0755 /opt/anet-hub/bin
# install -m 0755 anet-hub anet-hub-admin /opt/anet-hub/bin/
# install -m 0755 deploy/hub-db-roll.sh /opt/anet-hub/bin/
# install -d -m 0700 /etc/anet-hub
# (umask 077; printf 'ADMIN_TOKEN=%s\n' "$(openssl rand -hex 32)" > /etc/anet-hub/admin.env)
# apt-get install --yes sqlite3

deploy/hub-db-roll.sh 位于 ANetHub 仓库中,需要 sqlite3 命令。数据目录由 systemd 根据下列单元中的 StateDirectory= 行创建。

hub 单元

/etc/systemd/system/anet-hub.service:

[Unit]
Description=ANet Hub
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=anet-hub
Group=anet-hub
StateDirectory=anet-hub
StateDirectoryMode=0700
ExecStart=/opt/anet-hub/bin/anet-hub \
  --addr 127.0.0.1:8088 \
  --data /var/lib/anet-hub \
  --public-url https://hub.example.com
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
UMask=0077

NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/anet-hub
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target

运维面单元

运维面从 /etc/anet-hub/admin.env 读取令牌。systemd 在降权之前读取该文件,因此文件可由 root 持有,权限为 0600。不使用控制台的 hub 可完全省略此单元。

/etc/systemd/system/anet-hub-admin.service:

[Unit]
Description=ANet Hub operator plane
After=network-online.target anet-hub.service
Wants=network-online.target

[Service]
Type=simple
User=anet-hub
Group=anet-hub
StateDirectory=anet-hub-admin
StateDirectoryMode=0700
EnvironmentFile=/etc/anet-hub/admin.env
ExecStart=/opt/anet-hub/bin/anet-hub-admin \
  --addr 127.0.0.1:8078 \
  --hub-data /var/lib/anet-hub \
  --data /var/lib/anet-hub-admin
Restart=on-failure
RestartSec=3
UMask=0077

NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/anet-hub /var/lib/anet-hub-admin
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target

文件中缺少 ADMIN_TOKEN 行,或其值为占位值时,服务不会启动。重启服务之前请先检查令牌:

# sh -c 'set -a; . /etc/anet-hub/admin.env; set +a; /opt/anet-hub/bin/anet-hub-admin --check-token' && echo accepted

维护任务

hub-db-roll.sh 对预写日志执行检查点、生成每周备份并清理旧备份(参见 备份与恢复),每天运行一次。

/etc/systemd/system/hub-db-roll.service:

[Unit]
Description=anet-hub database maintenance
After=anet-hub.service

[Service]
Type=oneshot
User=anet-hub
Group=anet-hub
Environment=HUB_DATA_DIR=/var/lib/anet-hub
ExecStart=/opt/anet-hub/bin/hub-db-roll.sh
Nice=10
IOSchedulingClass=idle
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/anet-hub
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX
LockPersonality=yes
CapabilityBoundingSet=

/etc/systemd/system/hub-db-roll.timer:

[Unit]
Description=Daily anet-hub database maintenance

[Timer]
OnCalendar=*-*-* 04:30:00
Persistent=true

[Install]
WantedBy=timers.target

启动全部服务

# systemctl daemon-reload
# systemctl enable --now anet-hub.service anet-hub-admin.service hub-db-roll.timer
# systemctl status anet-hub.service
# curl -s http://127.0.0.1:8088/healthz
{"built_at":"2026-10-16T00:00:00Z","commit":"1a2b3c4","status":"ok","version":"0.3.0"}

随后设置 hub 身份的恢复口令。此步骤须在 hub 停止时执行,且命令必须在终端中运行。恢复文件的位置应选在数据目录之外:

# systemctl stop anet-hub.service
# install -d -o anet-hub -g anet-hub -m 0700 /etc/anet-hub/recovery
# sudo -u anet-hub /opt/anet-hub/bin/anet-hub --data /var/lib/anet-hub \
      identity setup-recovery --recovery-file /etc/anet-hub/recovery/hub.anetrec
# systemctl start anet-hub.service

将 hub.anetrec 复制到独立的存储中。由于服务账户没有主目录,之后的 identity status 与 identity rotate 命令须通过 --recovery-file 指定同一文件。其他运维子命令同样以服务账户的身份运行:

# sudo -u anet-hub /opt/anet-hub/bin/anet-hub --data /var/lib/anet-hub admissions

最后,按 TLS 与反向代理 所示的反向代理配置发布 hub,并为该主机名申请证书。

使用容器部署 hub

项目不发布容器镜像。下面的 Containerfile 围绕静态编译的 anet-hub 二进制文件(使用 CGO_ENABLED=0 构建)构建最小镜像。镜像不含 shell,仅包含该二进制文件和一个空的可写 /data 目录。

FROM debian:12-slim AS prepare
RUN mkdir /data

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=prepare --chown=65532:65532 /data /data
COPY anet-hub /usr/local/bin/anet-hub
VOLUME /data
EXPOSE 8088
ENTRYPOINT ["/usr/local/bin/anet-hub", "--addr", ":8088", "--data", "/data"]

构建并启动。端口仅发布在宿主机的回环接口上,因此反向代理仍是唯一的公开入口:

$ CGO_ENABLED=0 go build -o anet-hub ./cmd/anet-hub
$ docker build -f Containerfile -t anet-hub:0.3.0 .
$ docker volume create anet-hub-data
$ docker run -d --name anet-hub --restart unless-stopped \
      -p 127.0.0.1:8088:8088 -v anet-hub-data:/data \
      anet-hub:0.3.0 --public-url https://hub.example.com
$ curl -s http://127.0.0.1:8088/healthz
{"built_at":"unknown","commit":"unknown","status":"ok","version":"0.3.0"}

镜像名之后的参数会追加到入口点,因此可在此处给出 --public-url 以及 运行 hub 中的其他选项。运维子命令在使用同一数据卷的第二个容器中运行。设置恢复口令时须停止 hub 容器,并将恢复文件保存在单独的绑定挂载上,该挂载归镜像用户(uid 65532)所有:

$ docker stop anet-hub
$ install -d -o 65532 -g 65532 -m 0700 recovery
$ docker run --rm -it --entrypoint /usr/local/bin/anet-hub \
      -v anet-hub-data:/data -v "$PWD/recovery:/recovery" anet-hub:0.3.0 \
      --data /data identity setup-recovery --recovery-file /recovery/hub.anetrec
$ docker start anet-hub
$ docker run --rm --entrypoint /usr/local/bin/anet-hub \
      -v anet-hub-data:/data anet-hub:0.3.0 --data /data admissions

hub 将日志写入标准错误输出,可用 docker logs anet-hub 查看。

使用 systemd 部署节点

需要在无登录用户的情况下运行的节点(例如公开能力的提供方)应以独立账户运行 anet daemon。数据目录为 ANET_DATA_DIR。命令行工具拒绝与以其他用户身份运行的守护进程通信,因此针对该节点的命令须以其账户身份执行。

# useradd --system --home-dir /var/lib/anet-node --shell /usr/sbin/nologin anet-node
# install -d -m 0755 /opt/anet/bin
# install -m 0755 anet /opt/anet/bin/anet

/etc/systemd/system/anet-node.service:

[Unit]
Description=ANet node
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=anet-node
Group=anet-node
StateDirectory=anet-node
StateDirectoryMode=0700
RuntimeDirectory=anet-node
RuntimeDirectoryMode=0700
Environment=HOME=/var/lib/anet-node
Environment=ANET_DATA_DIR=/var/lib/anet-node/data
Environment=XDG_RUNTIME_DIR=/run/anet-node
ExecStart=/opt/anet/bin/anet daemon
Restart=on-failure
RestartSec=5s
UMask=0077

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
ProtectSystem=strict
ReadWritePaths=/var/lib/anet-node
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
SystemCallErrorNumber=EPERM

[Install]
WantedBy=multi-user.target

RestrictNamespaces=yes 与系统调用过滤会使自动回复模块的沙箱无法启动,因为沙箱需要创建命名空间。使用沙箱化本地 Agent 应答任务的节点,需要放宽这两行配置(参见 沙箱与凭据)。

一个简短的包装脚本以节点账户和相同的环境运行命令行工具:

/usr/local/bin/anet-node:

#!/bin/sh
exec runuser -u anet-node -- env \
  HOME=/var/lib/anet-node \
  ANET_DATA_DIR=/var/lib/anet-node/data \
  XDG_RUNTIME_DIR=/run/anet-node \
  /opt/anet/bin/anet "$@"
# chmod 0755 /usr/local/bin/anet-node
# systemctl daemon-reload
# systemctl enable --now anet-node.service
# anet-node status
# anet-node hub-register https://hub.example.com --name provider-1

hub-register 在终端中请求输入恢复口令,并将恢复文件写入数据目录之外的 /var/lib/anet-node/.anet-recovery/。请将该文件复制到独立存储(参见 备份与恢复)。在没有终端时,注册仍可完成,但节点会报告该身份尚无恢复口令,直至在终端中运行 anet identity protect。

使用容器部署节点

节点可采用相同的方式。镜像包含静态编译的 anet 二进制文件、存放节点数据的可写 /data 目录,以及接收恢复文件的可写 /recovery 目录。两者应使用各自独立的数据卷,以便分别备份和保存。

FROM debian:12-slim AS prepare
RUN mkdir /data /recovery

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=prepare --chown=65532:65532 /data /data
COPY --from=prepare --chown=65532:65532 /recovery /recovery
COPY anet /usr/local/bin/anet
ENV ANET_DATA_DIR=/data HOME=/recovery
VOLUME ["/data", "/recovery"]
ENTRYPOINT ["/usr/local/bin/anet"]
CMD ["daemon"]
$ CGO_ENABLED=0 go build -o anet ./cmd/anet
$ docker build -f Containerfile -t anet-node:0.3.0 .
$ docker run -d --name anet-node --restart unless-stopped \
      -v anet-node-data:/data -v anet-node-recovery:/recovery anet-node:0.3.0
$ docker exec -it anet-node anet hub-register https://hub.example.com --name provider-1
$ docker exec anet-node anet doctor

控制面与本地 A2A 接口监听容器自身的回环接口,不对外发布;命令通过 docker exec 访问它们。请勿发布这些端口。

检查清单

hub 接受来自互联网的节点之前,请确认:

  1. /healthz 可通过公开域名以 HTTPS 访问,且返回的版本为最近安装的版本。

  2. 代理设置了 X-Real-IP,允许的请求体不小于 --relay-max-envelope,并且不保留访问日志。

  3. hub 身份已设置恢复口令,且恢复文件的副本存放在主机之外。

  4. 若使用运维面,它仅通过代理应答,其令牌不是占位值,也未与其他系统共用。

  5. 每日维护任务已运行过一次(数据目录中的 roll.log),并且备份已在测试主机上恢复验证(备份与恢复)。

  6. anet-hub admission 显示的是预期的模式,且 federation.json 恰好列出预期的对等方(联邦)。