feat: add admin API IP whitelist (#1131)

* feat: add admin API IP whitelist

* fix: harden admin IP whitelist checks
This commit is contained in:
Dream Hunter
2026-09-02 01:21:24 +08:00
committed by GitHub
parent 5fd181d9c0
commit 3c505db9d0
12 changed files with 35 additions and 3 deletions
+1
View File
@@ -13,6 +13,7 @@
- feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态 - feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态
- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 - feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限
- feat: |Admin| 创建邮箱页面支持一键生成随机邮箱名称(issue #1126 - feat: |Admin| 创建邮箱页面支持一键生成随机邮箱名称(issue #1126
- feat: |Admin API| 新增 `ADMIN_API_IP_WHITELIST`,可按来源 IP 限制所有管理接口访问
- feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API - feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API
### Bug Fixes ### Bug Fixes
+1
View File
@@ -13,6 +13,7 @@
- feat: |Mail| Add optional read/unread status with click-to-read and manual status switching - feat: |Mail| Add optional read/unread status with click-to-read and manual status switching
- feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit - feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit
- feat: |Admin| Add one-click random email-name generation to the address creation page (issue #1126) - feat: |Admin| Add one-click random email-name generation to the address creation page (issue #1126)
- feat: |Admin API| Add `ADMIN_API_IP_WHITELIST` to restrict all admin endpoints by source IP
- feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management - feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management
### Bug Fixes ### Bug Fixes
@@ -67,6 +67,8 @@ JWT_SECRET = "xxx"
# Admin console password, if not configured, console access is not allowed # Admin console password, if not configured, console access is not allowed
# ADMIN_PASSWORDS = ["123", "456"] # ADMIN_PASSWORDS = ["123", "456"]
# Admin API IP whitelist; applies to admin-password and Admin user-token access
# ADMIN_API_IP_WHITELIST = ["203.0.113.10"]
# Whether to allow users to create emails, not allowed if not configured # Whether to allow users to create emails, not allowed if not configured
ENABLE_USER_CREATE_EMAIL = true ENABLE_USER_CREATE_EMAIL = true
@@ -23,8 +23,11 @@
| Variable Name | Type | Description | Example | | Variable Name | Type | Description | Example |
| ------------------------------ | --------- | ------------------------------------------------------- | ---------------- | | ------------------------------ | --------- | ------------------------------------------------------- | ---------------- |
| `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` | | `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` |
| `ADMIN_API_IP_WHITELIST` | JSON | Admin API IP whitelist; when configured, only listed IPs may access `/admin/*` | `["203.0.113.10"]` |
| `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` | | `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` |
When `ADMIN_API_IP_WHITELIST` is unset or empty, source IPs are not restricted. Once configured, it applies to both admin-password and Admin user-token access, trusts only Cloudflare's `CF-Connecting-IP` header, and denies requests without that header.
## Email Related Variables ## Email Related Variables
| Variable Name | Type | Description | Example | | Variable Name | Type | Description | Example |
@@ -67,6 +67,8 @@ JWT_SECRET = "xxx"
# admin 控制台密码, 不配置则不允许访问控制台 # admin 控制台密码, 不配置则不允许访问控制台
# ADMIN_PASSWORDS = ["123", "456"] # ADMIN_PASSWORDS = ["123", "456"]
# Admin API IP 白名单,同时限制管理员密码和 Admin 用户令牌访问
# ADMIN_API_IP_WHITELIST = ["203.0.113.10"]
# 是否允许用户创建邮件, 不配置则不允许 # 是否允许用户创建邮件, 不配置则不允许
ENABLE_USER_CREATE_EMAIL = true ENABLE_USER_CREATE_EMAIL = true
@@ -23,8 +23,11 @@
| 变量名 | 类型 | 说明 | 示例 | | 变量名 | 类型 | 说明 | 示例 |
| ------------------------------ | --------- | ------------------------------------ | ---------------- | | ------------------------------ | --------- | ------------------------------------ | ---------------- |
| `PASSWORDS` | JSON | 网站私有密码, 配置后需要密码才能访问 | `["123", "456"]` | | `PASSWORDS` | JSON | 网站私有密码, 配置后需要密码才能访问 | `["123", "456"]` |
| `ADMIN_API_IP_WHITELIST` | JSON | Admin API IP 白名单,配置后所有 `/admin/*` 请求仅允许列表中的 IP | `["203.0.113.10"]` |
| `DISABLE_ADMIN_PASSWORD_CHECK` | 文本/JSON | 警告: 管理员控制台没有密码或用户检查 | `false` | | `DISABLE_ADMIN_PASSWORD_CHECK` | 文本/JSON | 警告: 管理员控制台没有密码或用户检查 | `false` |
`ADMIN_API_IP_WHITELIST` 未配置或为空数组时不限制来源 IP。配置后,它会同时限制管理员密码和 Admin 用户令牌访问,只信任 Cloudflare 提供的 `CF-Connecting-IP`,缺少该请求头也会拒绝访问。
## 邮箱相关变量 ## 邮箱相关变量
| 变量名 | 类型 | 说明 | 示例 | | 变量名 | 类型 | 说明 | 示例 |
+1
View File
@@ -6,6 +6,7 @@ const messages: LocaleMessages = {
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page", UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page", UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page",
NeedAdminPasswordMsg: "You need to provide the admin password to access this page", NeedAdminPasswordMsg: "You need to provide the admin password to access this page",
AdminApiIpNotAllowedMsg: "Your IP address isn't on the admin whitelist",
KVNotAvailableMsg: "KV is not available, please contact the administrator", KVNotAvailableMsg: "KV is not available, please contact the administrator",
DBNotAvailableMsg: "DB is not available, please contact the administrator", DBNotAvailableMsg: "DB is not available, please contact the administrator",
+1
View File
@@ -4,6 +4,7 @@ export type LocaleMessages = {
UserAcceesTokenExpiredMsg: string UserAcceesTokenExpiredMsg: string
UserRoleIsNotAdminMsg: string UserRoleIsNotAdminMsg: string
NeedAdminPasswordMsg: string NeedAdminPasswordMsg: string
AdminApiIpNotAllowedMsg: string
KVNotAvailableMsg: string KVNotAvailableMsg: string
DBNotAvailableMsg: string DBNotAvailableMsg: string
+1
View File
@@ -6,6 +6,7 @@ const messages: LocaleMessages = {
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面", UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问", UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问",
NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面", NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面",
AdminApiIpNotAllowedMsg: "当前 IP 不在管理员白名单中",
KVNotAvailableMsg: "KV 不可用, 请联系管理员", KVNotAvailableMsg: "KV 不可用, 请联系管理员",
DBNotAvailableMsg: "DB 不可用, 请联系管理员", DBNotAvailableMsg: "DB 不可用, 请联系管理员",
+1
View File
@@ -49,6 +49,7 @@ type Bindings = {
DOMAIN_LABELS: string | string[] | undefined DOMAIN_LABELS: string | string[] | undefined
PASSWORDS: string | string[] | undefined PASSWORDS: string | string[] | undefined
ADMIN_PASSWORDS: string | string[] | undefined ADMIN_PASSWORDS: string | string[] | undefined
ADMIN_API_IP_WHITELIST: string | string[] | undefined
DISABLE_ADMIN_PASSWORD_CHECK: string | boolean | undefined DISABLE_ADMIN_PASSWORD_CHECK: string | boolean | undefined
JWT_SECRET: string JWT_SECRET: string
BLACK_LIST: string | undefined BLACK_LIST: string | undefined
+17 -3
View File
@@ -14,7 +14,7 @@ import { api as telegramApi } from './telegram_api'
import i18n from './i18n'; import i18n from './i18n';
import { email } from './email'; import { email } from './email';
import { scheduled } from './scheduled'; import { scheduled } from './scheduled';
import { getPasswords, getBooleanValue, getDomains, checkIsAdmin } from './utils'; import { getPasswords, getBooleanValue, getDomains, checkIsAdmin, getEnvStringList } from './utils';
import { checkAccessControl } from './ip_blacklist'; import { checkAccessControl } from './ip_blacklist';
const API_PATHS = [ const API_PATHS = [
@@ -221,14 +221,28 @@ app.use('/user_api/*', async (c, next) => {
}); });
// admin auth // admin auth
app.use('/admin/*', async (c, next) => { app.use('/admin/*', async (c, next) => {
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
try {
const ipWhitelist = getEnvStringList(c.env.ADMIN_API_IP_WHITELIST)
.filter(ip => typeof ip === "string")
.map(ip => ip.trim())
.filter(Boolean);
if (ipWhitelist.length > 0) {
const reqIp = c.req.raw.headers.get("cf-connecting-ip")?.trim();
if (!reqIp || !ipWhitelist.includes(reqIp)) {
return c.text(msgs.AdminApiIpNotAllowedMsg, 403);
}
}
} catch (e) {
console.error("Failed to check admin API IP whitelist", e);
}
// check header x-admin-auth // check header x-admin-auth
if (checkIsAdmin(c)) { if (checkIsAdmin(c)) {
await next(); await next();
return; return;
} }
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
// check if user is admin // check if user is admin
const access_token = c.req.raw.headers.get("x-user-access-token"); const access_token = c.req.raw.headers.get("x-user-access-token");
if (c.env.ADMIN_USER_ROLE && access_token) { if (c.env.ADMIN_USER_ROLE && access_token) {
+2
View File
@@ -45,6 +45,8 @@ PREFIX = "tmp"
# PASSWORDS = ["123", "456"] # PASSWORDS = ["123", "456"]
# For admin panel # For admin panel
# ADMIN_PASSWORDS = ["123", "456"] # ADMIN_PASSWORDS = ["123", "456"]
# Restrict all /admin/* APIs to exact client IPv4 or IPv6 addresses
# ADMIN_API_IP_WHITELIST = ["203.0.113.10", "2001:db8::1"]
# warning: no password or user check for admin portal # warning: no password or user check for admin portal
# DISABLE_ADMIN_PASSWORD_CHECK = false # DISABLE_ADMIN_PASSWORD_CHECK = false
# ADMIN CONTACT, CAN BE ANY STRING # ADMIN CONTACT, CAN BE ANY STRING