绿联自动更新tailscale证书
起因
绿联上部署了Vaultwarden,但是很蛋疼的一点就是Vaultwarden 必须使用https才能同步,没办法只能在nginx中做代理配置上证书,好在tailscale可以提供免费证书。
用了一段时间后发现Vaultwarden突然同步不了了,看了看发现是证书过期了,不是哥们!我局域网证书还过期😅 没招了 只能重新生成证书替换,重启nginx,搞定! 然后三个月后一次下午又过期了。。。这种情况真的有点下头了 。还是配个脚本来自动更新吧,
首先说下环境,我的tailscale和nginx都部署在docker容器中
| 容器 | 网络 | 配置 | 说明 |
|---|---|---|---|
| nginx | bridge | 映射: /etc/ningx/conf.d /usr/share/nginx/certs /usr/share/nginx/html | 反向代理请求升级https到Vaultwarden |
| tailscale | host | 映射: /dev/net/tun /var/lib | 局域网穿透 |
nginx配置文件如下:
server {
listen 41101 ssl; # Nginx 容器监听 443 端口(HTTPS)
server_name xxx.net; # Tailscale 域名
# 证书路径(容器内路径,对应宿主机 ~/docker/apps/tailscale/lib/tailscale/certs)
ssl_certificate /usr/share/nginx/certs/xxx.crt;
ssl_certificate_key /usr/share/nginx/certs/xxx.key;
# 反向代理到 Vaultwarden
location / {
proxy_pass http://ip:port;
# 必要的代理头(确保Vaultwarden获取正确的客户端信息)
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;
}
}
手动更新证书的流程就是进入tailscale容器执行tailscale cert 域名命令生成证书,生成后的证书在容器的/var/lib/tailscale/certs目录下,因为映射了/var/lib目录到宿主机,又把这个目录映射到了nginx容器的/usr/share/nginx/certs/目录 ,所以生成证书后就重新替换了nginx的证书,接下来重启nginx容器就可以重新加载新的证书了。
因为绿联官方没有定时执行脚本的功能,只能自己写一个脚本在绿联机器执行cron任务来实现了。
开始实现
实现思路是:每周一凌晨4点检查一次证书剩余有效期,只有证书不存在、已过期或剩余天数小于阈值时,才去执行 tailscale cert 并重启 Nginx。这样既能自动续期,又不会频繁重启 Nginx。
- 宿主机上由 cron 触发脚本。
- 脚本检查宿主机上 Nginx 实际读取的证书文件。
- 用
openssl x509 -checkend判断证书是否会在指定天数内过期。 - 如果证书还很久才过期 → 打印日志,直接退出。
- 如果证书不存在/即将过期/已过期 → 进入 Tailscale 容器执行
tailscale cert 域名。 - 验证新证书 是否生成且有效期足够。
- 重启 Nginx 容器,让新证书生效。
- 日志同时输出到终端和日志文件。
确认依赖
宿主机上需要有:
bashdockeropenssldatetee
绿联一般都有。可以在终端执行:
which docker openssl date tee
如果都有输出就没问题。
脚本内容
#!/bin/bash
# ================== 可配置变量 ==================
TAILSCALE_CONTAINER="tailscale" # Tailscale 容器名称
NGINX_CONTAINER="nginx" # Nginx 容器名称
DOMAIN="xxx.net" # Tailscale 域名
CERT_HOST_DIR="/volume1/docker/apps/tailscale/lib/tailscale/certs" # 宿主机证书目录
CERT_FILE="${CERT_HOST_DIR}/${DOMAIN}.crt" # 证书文件,需与 Nginx 配置一致
KEY_FILE="${CERT_HOST_DIR}/${DOMAIN}.key" # 私钥文件
DAYS_THRESHOLD=7 # 剩余天数小于此值则续期
LOG_FILE="/volume1/docker/apps/renew_tailscale_cert/tailscale-cert-renew.log"
# ================================================
# 确保 cron 环境下也能找到 docker 等命令
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# 日志目录
mkdir -p "$(dirname "$LOG_FILE")"
# 日志函数:同时输出到终端和日志文件
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE"
}
log_warn() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" | tee -a "$LOG_FILE"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >&2
}
log_info "========== 开始检查 Tailscale 证书 =========="
log_info "域名:$DOMAIN"
log_info "证书路径:$CERT_FILE"
log_info "续期阈值:剩余 $DAYS_THRESHOLD 天"
NEED_RENEW=0
# 1. 检查证书是否存在
if [ ! -f "$CERT_FILE" ]; then
log_warn "证书文件不存在,将执行首次生成。"
NEED_RENEW=1
else
# 打印当前证书到期时间
EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$CERT_FILE" 2>/dev/null | cut -d= -f2)
log_info "当前证书到期时间:${EXPIRY_DATE:-未知}"
# 2. 判断证书是 否在阈值天数内过期
if openssl x509 -checkend $((DAYS_THRESHOLD * 86400)) -noout -in "$CERT_FILE" >/dev/null 2>&1; then
log_info "证书距离过期还有超过 $DAYS_THRESHOLD 天,无需更新。"
log_info "========== 检查结束 =========="
exit 0
else
log_warn "证书已过期或将在 $DAYS_THRESHOLD 天内过期,需要续期。"
NEED_RENEW=1
fi
fi
# 3. 执行续期
if [ "$NEED_RENEW" -eq 1 ]; then
log_info "在容器 $TAILSCALE_CONTAINER 中执行:tailscale cert $DOMAIN"
if docker exec "$TAILSCALE_CONTAINER" tailscale cert "$DOMAIN" >> "$LOG_FILE" 2>&1; then
log_info "tailscale cert 命令执行成功。"
else
log_error "tailscale cert 命令执行失败,请检查 Tailscale 容器状态和日志。"
exit 1
fi
# 4. 验证新证书是否生成
if [ ! -f "$CERT_FILE" ]; then
log_error "续期后证书文件仍不存在:$CERT_FILE"
exit 1
fi
# 5. 再次检查新证书有效期
NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$CERT_FILE" 2>/dev/null | cut -d= -f2)
if openssl x509 -checkend $((DAYS_THRESHOLD * 86400)) -noout -in "$CERT_FILE" >/dev/null 2>&1; then
log_info "新证书验证通过,到期时间:${NEW_EXPIRY_DATE:-未知}"
else
log_error "新证书验证失败,可能仍未成功续期。到期时间:${NEW_EXPIRY_DATE:-未知}"
exit 1
fi
# 6. 重启 Nginx 容器
log_info "重启 Nginx 容器:$NGINX_CONTAINER"
if docker restart "$NGINX_CONTAINER" >> "$LOG_FILE" 2>&1; then
log_info "Nginx 容器重启成功,证书更新完成。"
else
log_error "Nginx 容器重启失败!"
exit 1
fi
fi
log_info "========== 证书检查/更新流程结束 =========="
exit 0
openssl x509 -checkend 秒数 -noout -in 证书文件 会检查证书是否在指定秒数后过期:
- 返回
0:证书在指定时间内不会过期。 - 返回
1:证书已过期,或将在指定时间内过期。
openssl x509 -checkend $((30 * 86400)) -noout -in "$CERT_FILE"
就是判断证书是否会在 30 天内过期。这样比用 date -d 解析日期更稳,跨平台兼容性更好。
创建定时任务:
先给脚本执行权限:
chmod +x renew_tailscale_cert.sh
然后编辑 cron:
sudo crontab -e
添加一行,每周一凌晨四点检查一次:
0 4 * * 1 /bin/bash /volume1/docker/apps/renew_tailscale_cert/renew_tailscale_cert.sh >/dev/null 2>&1
因为脚本内部已经写日志了,所以把 cron 的 stdout/stderr 重定向到 /dev/null。
查看是否配置成功:
sudo crontab -l
查看服务是否正常:
sudo systemctl status cron
先手动运行一次:
sudo /bin/bash /volume1/docker/apps/renew_tailscale_cert/renew_tailscale_cert.sh
会看到类似输出:
[2026-09-10 17:35:50] [INFO] ========== 开始检查 Tailscale 证书 ==========
[2026-09-10 17:35:50] [INFO] 域名:dh4300plus.tail6b1815.ts.net
[2026-09-10 17:35:50] [INFO] 证书路径:/volume1/docker/apps/tailscale/lib/tailscale/certs/xxx.crt
[2026-09-10 17:35:50] [INFO] 续期阈值:剩余 15 天
[2026-09-10 17:35:50] [INFO] 当前证书到期时间:Dec 9 07:00:55 2026 GMT
[2026-09-10 17:35:50] [INFO] 证书距离过期还有超过 15 天,无需更新。
[2026-09-10 17:35:50] [INFO] ========== 检查结束 ==========
注意
tailscale cert 对仍有效的证书不会重新签发,只要当前证书还在有效期内,tailscale cert 就不会重新生成,除非你显式强制。目的是避免频繁申请 Let's Encrypt 证书、触发速率限制。
这个“需要续期”的判断阈值,根据 Tailscale 的版本和策略不同,通常是:
- 证书到期前 14 天:这是 Tailscale 早期硬编码的续期逻辑。
- 证书剩余有效期低于 1/3:对于 90 天有效期的证书,大约也是在到期前 30 天左右。