From ea9c885a15c5b1a6b0115e59e810fe494e563ced Mon Sep 17 00:00:00 2001 From: opsx Date: Thu, 22 Oct 2026 08:03:08 +0800 Subject: [PATCH] Show OpenVPN OIDC prerequisites and exposed management port --- backend/internal/api/api.go | 1 + .../internal/api/openvpn_oidc_readiness.go | 46 +++++++++++++++++++ backend/internal/tlsservice/tlsservice.go | 14 +++++- .../internal/tlsservice/tlsservice_test.go | 6 ++- docs/specs/SDD-001-openvpn-oidc.md | 9 +++- docs/specs/SDD-004-nginx-acme-entry.md | 1 + frontend/src/components/OpenVPNOIDCGuide.vue | 11 ++++- frontend/src/locales/en-US.json | 3 ++ frontend/src/locales/zh-CN.json | 3 ++ frontend/src/views/Settings.vue | 1 + 10 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 backend/internal/api/openvpn_oidc_readiness.go diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 8432277..a9d9e7b 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -285,6 +285,7 @@ func (s *Server) RegisterRoutes(r *gin.Engine) { ops.POST("/binaries/install/:name", s.installBinary) ops.GET("/binaries/install-hint/:name", s.binaryInstallHint) ops.PUT("/tunnels/:id/policy", s.saveTrafficPolicy) + ops.GET("/tunnels/openvpn/oidc-readiness", s.openvpnOIDCReadiness) // 批量操作 & 任务管理 ops.POST("/tunnels/batch/start", s.batchTunnels(taskmgr.ActionStart)) diff --git a/backend/internal/api/openvpn_oidc_readiness.go b/backend/internal/api/openvpn_oidc_readiness.go new file mode 100644 index 0000000..af2b50b --- /dev/null +++ b/backend/internal/api/openvpn_oidc_readiness.go @@ -0,0 +1,46 @@ +package api + +import ( + "context" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/gin-gonic/gin" + "vpn-platform/backend/internal/nginxgateway" + "vpn-platform/backend/internal/service" + "vpn-platform/backend/internal/tlsservice" +) + +var oidcVersionPattern = regexp.MustCompile(`(?m)^version: ([0-9][0-9A-Za-z.+-]{1,32})\s*$`) + +// This checks local prerequisites only. It never tests credentials or claims +// that an OIDC issuer, callback domain or user authorization is configured. +func (s *Server) openvpnOIDCReadiness(c *gin.Context) { + path := filepath.Join(service.BaseDir(), "bin", "openvpn-auth-oauth2") + info, err := os.Stat(path) + if err != nil || !info.Mode().IsRegular() || info.Mode()&0o111 == 0 { + path, _ = service.LookBin("openvpn-auth-oauth2") + } + version := "" + if path != "" { + ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) + defer cancel() + if output, err := exec.CommandContext(ctx, path, "--version").Output(); err == nil { + if match := oidcVersionPattern.FindSubmatch(output); len(match) == 2 { + version = strings.TrimSpace(string(match[1])) + } + } + } + tls := tlsservice.Status() + c.JSON(http.StatusOK, gin.H{ + "component_installed": path != "", "component_version": version, + "https_management_ready": tls.Mode == "nginx" && tls.Valid && nginxgateway.Stage() == "https", + "management_domain": tls.Domain, + "authentication_entry_ready": false, // Depends on a distinct callback site and a configured provider. + }) +} diff --git a/backend/internal/tlsservice/tlsservice.go b/backend/internal/tlsservice/tlsservice.go index fde8e8c..ba01796 100644 --- a/backend/internal/tlsservice/tlsservice.go +++ b/backend/internal/tlsservice/tlsservice.go @@ -51,6 +51,7 @@ type CertificateStatus struct { NotBefore time.Time `json:"not_before,omitempty"` NotAfter time.Time `json:"not_after,omitempty"` Renewal string `json:"renewal_mode"` + DirectPortPublic bool `json:"management_direct_public"` } func ValidDomain(domain string) bool { @@ -222,6 +223,15 @@ func Status() CertificateStatus { config := state.config state.RUnlock() status := CertificateStatus{Enabled: config.domain != "" && config.mode == "autocert", Domain: config.domain, Mode: config.mode, DNS: []string{}, HTTPAddr: config.httpAddr, HTTPSAddr: config.httpsAddr, Renewal: config.mode} + if config.mode == "nginx" { + listenAddress := os.Getenv("VPN_LISTEN_ADDR") + if listenAddress == "" { + listenAddress = ":18080" + } + if host, _, err := net.SplitHostPort(listenAddress); err == nil { + status.DirectPortPublic = host == "" || host == "0.0.0.0" || host == "::" + } + } if desired, err := configFromEnvironment(config.dataDir); err == nil { status.ConfiguredDomain = desired.domain status.ConfiguredMode = desired.mode @@ -233,7 +243,9 @@ func Status() CertificateStatus { status.Domain = "" if config.domain != "" { readCertificate(filepath.Join("/etc/letsencrypt/live", config.domain, "fullchain.pem"), config.domain, &status) - if status.Valid { status.Domain = config.domain } + if status.Valid { + status.Domain = config.domain + } } return status } diff --git a/backend/internal/tlsservice/tlsservice_test.go b/backend/internal/tlsservice/tlsservice_test.go index a28b99e..4228d89 100644 --- a/backend/internal/tlsservice/tlsservice_test.go +++ b/backend/internal/tlsservice/tlsservice_test.go @@ -80,7 +80,11 @@ func TestNginxModeDoesNotBindGoListeners(t *testing.T) { t.Fatal(err) } status := Status() - if status.Mode != "nginx" || status.Enabled || status.Domain != "" || status.ConfiguredDomain != "test-link.appx365.com" { + if status.Mode != "nginx" || status.Enabled || status.Domain != "" || status.ConfiguredDomain != "test-link.appx365.com" || !status.DirectPortPublic { t.Fatalf("proxy mode must not claim Go listens: %+v", status) } + t.Setenv("VPN_LISTEN_ADDR", "127.0.0.1:18080") + if Status().DirectPortPublic { + t.Fatal("loopback management listener must not be marked public") + } } diff --git a/docs/specs/SDD-001-openvpn-oidc.md b/docs/specs/SDD-001-openvpn-oidc.md index beedfce..4e6a727 100644 --- a/docs/specs/SDD-001-openvpn-oidc.md +++ b/docs/specs/SDD-001-openvpn-oidc.md @@ -4,7 +4,7 @@ |------|------| | 版本 / 变更级别 | 0.1 / S3(身份认证、部署) | | 设计状态 | Draft | -| 实现状态 | OpenVPN WebAuth 配置与认证组件进程管理已实现;真实飞书登录仍待 OIDC 提供方、HTTPS 入口和组件部署联调 | +| 实现状态 | OpenVPN WebAuth 配置与认证组件进程管理已实现;2026-10-22 已安装上游 openvpn-auth-oauth2 v2.3.0,真实飞书登录仍待 OIDC 提供方与独立认证入口联调 | | 负责人 / 日期 | 待指定 / 2026-10-20 | | 关联需求 | 创建 OpenVPN 专线时配置飞书 OIDC 浏览器认证 | | 批准状态 | 未批准;用户已明确要求开始开发 | @@ -32,7 +32,7 @@ - 安全边界:OIDC 的人员授权以飞书应用可用范围或身份代理群组为准;本期不以现有 LDAP 用户表做未经验证的身份匹配。 ## 4. 接口、数据与配置契约 -- 接口变更:现有隧道 API `config.auth_mode` 新值 `oidc`;新增 `GET /api/tunnels/openvpn/oidc-port`(可选 `from`、`direction`)推荐空闲本机端口;不新增虚假的 `/oauth2/callback` 后端接口。 +- 接口变更:现有隧道 API `config.auth_mode` 新值 `oidc`;新增 `GET /api/tunnels/openvpn/oidc-port`(可选 `from`、`direction`)推荐空闲本机端口,以及只读 `GET /api/tunnels/openvpn/oidc-readiness` 展示组件版本与管理 HTTPS 前提(不代表认证入口或发行者已配置);不新增虚假的 `/oauth2/callback` 后端接口。 - 数据模型 / 迁移:沿用隧道配置 JSON 与 Secrets 密文,无强制数据库迁移。 - 配置变更:OIDC issuer、client ID、client secret、外网 HTTPS base URL、认证服务本地监听端口;生成认证服务配置和管理 socket 密码文件。 - 兼容性:其余认证模式行为不变;OIDC 仅供支持 WebAuth 的客户端使用。 @@ -77,3 +77,8 @@ - 对外回调为 `https://<认证入口>/oauth2/callback`,由 `openvpn-auth-oauth2` 处理;代理方案还要把**代理提供给飞书的上游回调**登记在飞书应用,两条回调不能互换。认证入口可与管理后台同机不同域名部署,需要 HTTPS 与完整认证组件路由转发,`127.0.0.1:19090` 不对公网开放。 - 2026-10-21 服务器检查:OpenVPN 2.6.19 已安装;`openvpn-auth-oauth2`、Nginx/Certbot 和 80/443 监听均未检测到。未安装组件、准备可访问的 issuer、TLS 证书、飞书应用发布与授权之前,不切换现有运行中的 OpenVPN 专线到 OIDC。先用独立测试专线验证允许与拒绝、断线重连,再变更生产线路。发现失败时停止测试专线并保留原密码专线,不将缺失组件视为已完成登录联调。 - 飞书应用密钥曾出现在交互截图中,管理员应轮换后仅输入后台密钥框;仓库、文档、日志和测试均不得包含实际密钥。 + +### 2026-10-22 进展 + +- 上游 v2.3.0 Linux amd64 发布包校验 SHA-256 后安装在服务器 `data/bin/openvpn-auth-oauth2`;以管理进程相同的 root 权限执行 `--version` 显示 2.3.0,未启动任何 OIDC 专线。现有生成配置的 `http.baseurl`、`oauth2.issuer/client` 与 `openvpn.addr/password` 字段和固定版本官方配置样例一致。 +- 管理域名 `test-link.appx365.com` 的 HTTPS 证书已签发、续签演练通过;它目前只转发管理页面/API,不作为认证组件的公网入口。管理页面区分“组件已安装”与“认证入口/发行者未就绪”,不得把前者当成已完成飞书登录。 diff --git a/docs/specs/SDD-004-nginx-acme-entry.md b/docs/specs/SDD-004-nginx-acme-entry.md index 1c36591..bd3bca0 100644 --- a/docs/specs/SDD-004-nginx-acme-entry.md +++ b/docs/specs/SDD-004-nginx-acme-entry.md @@ -37,3 +37,4 @@ `return 444` 是 Nginx 的断连响应,不是权限控制;合法接口仍必须由 Gin 鉴权。Go `autocert` 模式不会提供 `return 444`。不能用路径白名单阻断 ACME 续期、MFA 或 WebAuth。认证域名与管理域名若合并,需要重新设计路径分发及 Cookie 隔离,不能直接套用独立域名配置。认证域名接入仍待独立域名和真实发行者信息;Certbot 续签 dry-run 与公网联调以实际执行记录为准。 当前 `:18080` 仍监听公网,旧客户端以 HTTP 直连会绕过 Nginx 的 444 白名单;迁移客户端服务地址至 HTTPS 后应单独收紧 18080。2026-10-22 本机对域名的第二轮公网探测出现连接重置/DNS 波动,服务器回环直连 HTTPS `/settings` 和管理健康检查正常;持续公网可用性需结合实际用户网络观察。 +设置页面显示这一直接端口暴露状态,防止管理员仅依据已签出的证书误认为外部访问都经过 Nginx。 diff --git a/frontend/src/components/OpenVPNOIDCGuide.vue b/frontend/src/components/OpenVPNOIDCGuide.vue index 33c459d..7d0980c 100644 --- a/frontend/src/components/OpenVPNOIDCGuide.vue +++ b/frontend/src/components/OpenVPNOIDCGuide.vue @@ -1,6 +1,7 @@