TLS-WebSocket连接失败报tls: unknown certificate错误求助
问题根因
WebSocket连接失败是两个独立配置错误共同导致的:
Nginx未配置后端代理规则
你当前的Nginx配置里仅在location /块中声明了WebSocket升级需要的协议头,但是没有配置proxy_pass将请求转发到Go服务监听的4000端口,反而配置了try_files直接返回本地静态文件,所有请求到Nginx就直接处理完了,根本不会到达后端Go服务,WebSocket自然不可能建立成功。
另外你原来写的try_files $uri $uri/ =404对Svelte这类单页应用也不兼容,前端路由刷新会直接返回404。Go服务TLS证书链不完整
你看到的http: TLS handshake error from [...] : remote error: tls: unknown certificate报错,是因为Go服务加载的是Let's Encrypt的cert.pem文件,这个文件仅包含站点的叶子证书,缺少Let's Encrypt的中间CA证书,客户端TLS握手时无法将证书回溯到受信任的根CA,就会判定证书未知不可信。你Nginx配置中使用的fullchain.pem才是包含完整证书链的文件,直接对外提供TLS服务时必须使用这个文件。
推荐解决方案(Nginx统一TLS终结,最易维护)
不要让Go服务直接对外暴露端口、单独处理TLS,统一由Nginx做443端口的TLS终结,静态资源由Nginx直接返回,WebSocket/API请求反向代理到本地监听的Go服务即可,Go服务不需要处理TLS,也不用单独维护证书,后续Let's Encrypt证书续期也不需要调整Go服务配置。
- 调整Go服务代码,仅监听本地回环地址,关闭TLS:
func (s Server) Init() { s.Sessions = make(map[string]*Session) http.HandleFunc("/", s.handler) // 仅监听本地127.0.0.1的4000端口,不对外暴露,不需要TLS err := http.ListenAndServe("127.0.0.1:4000", nil) if err != nil { fmt.Println(err) } }
- 调整Nginx配置,区分静态资源和代理请求:
server { root /home/ubuntu/actions-runner/_work/client/client/public; index index.html index.htm index.nginx-debian.html; server_name myserver.io www.myserver.io; # 静态资源直接由Nginx返回,SPA路由统一回退到index.html location / { try_files $uri $uri/ /index.html; } # WebSocket请求反代到本地Go服务,此处假设你的WebSocket服务挂载在/ws路径 # 如果你的WebSocket服务直接挂载在根路径,调整location匹配规则优先级即可 location /ws { proxy_pass http://127.0.0.1:4000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/myserver.io/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/myserver.io/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = www.myserver.io) { return 301 https://$host$request_uri; } # managed by Certbot if ($host = myserver.io) { return 301 https://$host$request_uri; } # managed by Certbot listen 80 ; listen [::]:80 ; server_name myserver.io www.myserver.io; return 404; # managed by Certbot }
- 调整前端Svelte客户端的WebSocket连接地址,不要直接带4000端口连接Go服务,直接连接同域的反代路径即可,比如
wss://myserver.io/ws,走默认443端口由Nginx处理。
备选方案(Go服务直接对外提供TLS)
如果因为特殊场景必须让Go直接监听4000端口对外提供TLS服务,只需要把Go代码中加载的证书文件从tls/cert.pem换成你Nginx用的fullchain.pem即可,同时保证运行Go服务的系统用户有读取Let's Encrypt证书目录的权限,否则会因为读不到证书文件启动失败。这个方案需要注意Let's Encrypt证书续期后Go服务要同步加载新证书,维护成本更高,不推荐使用。
内容的提问来源于stack exchange,提问作者Max Beier

