Neo4j 图数据库加固实战:认证与查询权限配置

Neo4j 安全加固常被简化成「改掉默认密码」,但真正导致图数据库被拖库的,往往是监听地址暴露在公网、Bolt 未启用 TLS、以及应用层用字符串拼接 Cypher 查询。本文按网络层、认证层、传输层、应用层的顺序,给出可直接落地的 Neo4j 5.x 加固配置,并说明社区版在没有细粒度 RBAC 时如何用多库与代理隔离来弥补。

适用场景

  • Neo4j 部署在云主机上,7474/7687 端口对公网可访问,安全组里长期留着 0.0.0.0/0。
  • 图数据库承载用户关系、设备关联、风控图谱等敏感数据,需要满足等保或内部审计要求。
  • 业务代码中存在 MATCH (u:User {name:'" + name + "'}) 这类拼接查询,存在被 Cypher 注入拖走全图的可能。
  • 使用社区版,无法使用细粒度角色,需要一套可执行的替代隔离方案。

前置条件

  • Neo4j 5.x(本文命令以 Debian/Ubuntu 的 tar 包或 apt 安装为例),配置目录 /etc/neo4j/。配置项在 5.x 中已从 dbms.connector.* 迁移到 server.*,照抄 4.x 文章会启动失败。
  • 具备 openssl(1.1.1 及以上,需支持 -addext)。
  • 有维护窗口,允许重启数据库服务。
  • 应用连接的驱动版本与 Neo4j 5.x 匹配(Java Driver 5.x / Python Driver 5.x)。

原理说明

Neo4j 默认安装后有三个暴露面:HTTP 7474 端口提供 Browser 控制台与 REST 接口,Bolt 7687 端口提供二进制协议供驱动连接,以及默认账号 neo4j/neo4j 在首次登录后必须改密。前两个是网络暴露,第三个是认证暴露,三者叠加就构成最常见的高危组合——攻击者用默认口令登录后即可执行任意 Cypher,包括删除全部节点。

加固思路是逐层收敛:网络层只监听回环或内网地址,非必要不开放 7474;认证层强制强口令并禁用默认账号的交互登录;传输层给 Bolt 配 TLS 并设为 REQUIRED,让明文连接直接握手失败;应用层用参数化查询消灭 Cypher 注入,同时对 LOAD CSV 与 APOC 高危过程做白名单限制。四层任何一层失守,其余三层仍能提供缓冲。

需要特别说明社区版的限制:Neo4j 社区版不提供细粒度 RBAC,所有用户实际都具备管理员权限。因此社区版的隔离不能靠授权,只能靠「多数据库 + 网络与代理隔离 + 应用层收口」三条替代路径。

操作步骤

步骤一:收敛监听地址

编辑 /etc/neo4j/neo4j.conf,先把网络面收到最小:

# 仅监听回环,外部访问一律走反向代理或内网
server.default_listen_address=127.0.0.1
server.bolt.listen_address=:7687
server.http.listen_address=:7474

# 对外只暴露 HTTPS,关闭明文 HTTP 控制台
server.https.enabled=true
server.https.listen_address=:7473
server.http.enabled=false

如果必须让应用跨主机连接 Bolt,把 server.default_listen_address 改为内网网卡地址(如 10.0.1.20),再用防火墙限定来源,不要直接改成 0.0.0.0:一旦绑定全地址,任何能路由到主机的 IP 都可尝试爆破。

步骤二:生成证书并启用 Bolt TLS

mkdir -p /var/lib/neo4j/certificates/bolt
cd /var/lib/neo4j/certificates/bolt

openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
  -keyout private.key -out public.crt \
  -subj "/CN=neo4j.internal.example.com" \
  -addext "subjectAltName=DNS:neo4j.internal.example.com,DNS:localhost"

chown -R neo4j:neo4j /var/lib/neo4j/certificates
chmod 600 private.key
chmod 644 public.crt

证书的 subjectAltName 必须与客户端连接时使用的主机名完全一致,这是启用 REQUIRED 后「连不上」的头号原因。在 neo4j.conf 中追加 SSL 策略:

dbms.ssl.policy.bolt.enabled=true
dbms.ssl.policy.bolt.base_directory=certificates/bolt
dbms.ssl.policy.bolt.private_key=private.key
dbms.ssl.policy.bolt.public_certificate=public.crt
dbms.ssl.policy.bolt.client_auth=NONE

dbms.ssl.policy.https.enabled=true
dbms.ssl.policy.https.base_directory=certificates/bolt
dbms.ssl.policy.https.private_key=private.key
dbms.ssl.policy.https.public_certificate=public.crt
dbms.ssl.policy.https.client_auth=NONE

# 明文 Bolt 一律拒绝
server.bolt.tls_level=REQUIRED

client_auth=NONE 表示只做服务端认证(客户端验证服务端证书),不要求客户端出示证书。若内网已有 CA,把 client_auth 改成 OPTIONAL 或 REQUIRE 可进一步实现双向认证,但需要为每个应用签发客户端证书。

步骤三:加固认证与关闭危险能力

dbms.security.auth_enabled=true

# 禁止 LOAD CSV 从远程 URL 拉取数据(SSRF 与数据外泄通道)
dbms.security.allow_csv_import_from_file_urls=false

# 过程白名单:只保留实际用到的 APOC 命名空间
dbms.security.procedures.allowlist=apoc.coll.*,apoc.map.*,apoc.text.*,apoc.date.*
dbms.security.procedures.unrestricted=

# 查询日志,便于事后追溯
dbms.logs.query.enabled=true
dbms.logs.query.threshold=1s

dbms.security.procedures.unrestricted 必须留空:它的语义是「这些过程可以突破沙箱限制」,一旦写入 apoc.*,等于把文件读写与网络访问能力全部交给任何能执行 Cypher 的人。修改配置后执行 systemctl restart neo4j,并在 /var/log/neo4j/debug.log 中确认没有 Unknown setting 报错。

步骤四:修改默认口令并禁用交互登录

# 首次改密(交互式,按提示输入新口令)
cypher-shell -a bolt://127.0.0.1:7687 -u neo4j -p neo4j

# 改用强口令并新建应用专用账号
ALTER CURRENT USER SET PASSWORD FROM 'neo4j' TO 'REDACTED-STRONG-PASS';
CREATE USER app_reader SET PASSWORD 'REDACTED-APP-PASS' SET PASSWORD CHANGE NOT REQUIRED;

口令建议 20 位以上、由密码管理器生成。应用侧账号不要与运维账号共用,便于后续按账号维度审计与快速吊销。

步骤五:消灭应用层 Cypher 注入

注入的成因与 SQL 注入完全一致:用户输入被拼进查询语句。下面这段 Python 代码是典型错误写法,攻击者提交 '}) DETACH DELETE u // 就能清空整张图。

# 危险:字符串拼接
q = "MATCH (u:User {email:'%s'}) RETURN u" % email
session.run(q)

正确做法是参数化,驱动会把值当作数据而非语法:

# 安全:参数化查询
session.run("MATCH (u:User {email:$email}) RETURN u", email=email)

标签名与关系类型无法参数化,必须用严格白名单校验后再拼接:

ALLOWED_LABELS = {"User", "Order", "Device"}
if label not in ALLOWED_LABELS:
    raise ValueError("illegal label")
session.run("MATCH (n:%s {id:$id}) RETURN n" % label, id=node_id)

另外要禁用或限制 LOAD CSV:它不仅能把远程文件拉进数据库,还能反向把查询结果写到外部地址。若业务确实需要导入,改用 neo4j-admin database import 离线导入,并放在单独实例上执行。

配置验证

  1. 端口只在本机监听:ss -lntp | grep -E '7474|7473|7687' 应显示 127.0.0.1 或内网地址;从外部执行 nmap -Pn -p 7474,7687 <公网IP> 结果应为 filtered。
  2. TLS 已生效:openssl s_client -connect neo4j.internal.example.com:7687 -showcerts < /dev/null 能返回证书链;cypher-shell -a neo4j://...:7687 明文连接应被拒绝。
  3. 加密连接可查:cypher-shell -a neo4j+s://neo4j.internal.example.com:7687 -u app_reader -p '***' "RETURN 1 AS ok;" 返回 1。
  4. 注入不生效:用测试账号执行参数化查询并传入 '}) DETACH DELETE u //,结果应为 0 行且数据未变;对照检查 dbms.logs.query 中的实际语句。
  5. 过程白名单正确:SHOW PROCEDURES YIELD name WHERE name STARTS WITH 'apoc' RETURN count(*); 返回数量应与白名单规模相近,未被放行的过程调用时报 not allowed。

常见问题

FAQ 1:社区版没有角色权限,多团队共用一个库怎么隔离?

三条路径组合使用:一是利用 多数据库(社区版 4.4 起支持),按业务线各建一个 database,应用连各自的库,互相看不到对方数据;二是网络隔离,每个业务只允许从自己的网段访问,用 nftables 按来源 IP 做限制;三是应用收口,禁止业务直连数据库,统一走后端服务,由服务层做实体级权限判断。若确实需要「只读用户」「按标签授权」这类能力,只能升级到 Enterprise 版,社区版无法通过配置实现。

FAQ 2:把 tls_level 改成 REQUIRED 后客户端连不上,怎么排查?

按顺序检查四点:连接串是否改成了 neo4j+s:// 或 bolt+s://(仍用 bolt:// 会在握手阶段被拒);证书 SAN 是否与连接使用的主机名一致(用 IP 连接就必须把 IP 写进 SAN);Java 客户端是否信任该证书(自签证书需导入 truststore,或把 CA 证书放进 cacerts);容器部署时证书路径是否挂载到了 certificates/bolt 目录。日志侧看 debug.log 中的 SSL handshake failed 与 certificate 关键字,基本能直接定位。

FAQ 3:漏洞扫描报告里 7474 未授权访问,但我们已经改了密码?

扫描器判定「未授权」的依据通常不是能否登录,而是端口可访问且返回了 Browser 页面或版本信息。彻底消除该告警的做法是把 server.http.enabled 设为 false,让 7474 根本不存在;需要图形界面时,通过 SSH 隧道访问 https://127.0.0.1:7473:ssh -L 7473:127.0.0.1:7473 user@db-host。这样既满足审计要求,也不牺牲运维便利。

总结

Neo4j 的安全水位取决于最薄弱的那一层。生产环境的加固清单可以压缩成五句话:监听地址只给内网、明文协议全关、Bolt 上 TLS 并设 REQUIRED、过程白名单留空 unrestricted、应用侧一律参数化。社区版缺少 RBAC 是客观限制,用多库加网络隔离能覆盖大部分场景;把这几项做完,图数据库就从「默认配置即高危」变成「需要真正拿下一个账号才能深入」的目标,攻击成本会显著抬升。