homelab 之 内网 https

  • 申请泛域名证书, aciton 自动更新.
  • 资料来源:

https://jixun.uk/posts/2023/configure-wildcard-ssl/
https://blog.baoshuo.ren/post/actions-ssl-cert/

  • 更新

    1
    2025.01.19 初始

导语

这一篇是 homelab 系列小结的开始, 最近彻底重构了网络和服务, 还有很多尚未成型, 因此以一些小篇节为主.

homelab 的内网服务最近开始先 docker 迁移, 遇到了一个很大问题: 自托管的 docker 仓库 http 需要在每个 client 配置允许不安全连接….简直要疯…

因此优先项成了: 完成内网 https

正文

需求

  • 申请泛域名证书, 自动更新.
  • https 反向代理

方案实际很容易确定:

  • 证书: 内网目前没有公开服务, 因此只需要一个泛域名证书, 这样维护成本也比较低
    • acme.sh 申请, Let’s Encrypt, cloudflare DNS 验证
    • 自动化托管到 gitea aciton(兼容 github action)
  • HTTPS 目前已有服务都支持反向代理, 那么 Traefik 就几乎唯一的选择了.

证书

背景参考: 使用 acme.sh 配置自动续签 SSL 证书 - 烧饼博客

首次申请

申请 Cloudflare API: 参考 使用 DNS 验证签发证书 (非常详细)

  • 不要申请成 Global API Key 杜绝额外安全性问题.

申请的 key 复制到当前 shell 下

1
2
3
export CF_Token="复制下来的 Token"
export CF_Account_ID="复制下来的 Account ID"
export CF_Zone_ID="复制下来的 Zone ID"

acme: 文档非常全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 会安装到 ~/.acme.sh/
# email 是有可能收到提醒的, 建议常用邮箱
curl https://get.acme.sh | sh -s [email protected]

# 设置为 letsencrypt
./acme.sh --set-default-ca --server letsencrypt

# DNS 验证, 申请泛域名
# zsh 下不能直接有 *, 因此加了 " "
./acme.sh --issue --dns dns_cf -d lan.jasper1024.com -d "*.lan.jasper1024.com"

# 安装搭配具体目录
./acme.sh --install-cert -d lan.jasper1024.com -d "*.lan.jasper1024.com" \
--cert-file ~/cert.pem \
--key-file ~/key.pem \
--fullchain-file ~/fullchain.pem

# 打包 ~/.acme.sh/account.conf -> base64
# 保存下这个字符串
tar cz ca account.conf | base64 -w0

Action

保存到 action 的密钥

  • base64 -> ACME_SH_ACCOUNT_TAR
  • CF_TOKEN
  • CF_ACCOUNT_ID
  • CF_ZONE_ID

主要配置文件参考自 使用 GitHub Actions 自动申请与部署 SSL 证书 - 宝硕博客

  • 证书直接推送到 certs 分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 名称
name: Issue SSL Certificates

# 触发条件
on:
# 手动运行
workflow_dispatch:
# 定时运行
schedule:
# 每两个月运行一次
- cron: "0 0 1 */2 *"
push:
branches:
- master

# 全局环境变量
env:
# Checkout 到的目录
CERTS_OUTPUT_BASE: certs
# 证书输出目录
CERTS_OUTPUT_DIRECTORY: lan.jasper1024.com
# 证书文件名
FILE_FULLCHAIN: fullchain.pem
# 私钥文件名
FILE_KEY: privatekey.key

jobs:
issue-ssl-certificate:
# 申请证书并 push 到 certs 分支
name: Issue SSL certificate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: master

- name: Checkout output branch
uses: actions/checkout@v2
with:
ref: certs
path: ${{ env.CERTS_OUTPUT_BASE }}

# 安装 acme.sh
- name: Install acme.sh
shell: bash
run: |
curl https://get.acme.sh | sh -s [email protected] --force
# 检查 acme.sh 是否存在
if [ ! -f ~/.acme.sh/acme.sh ]; then
echo "acme.sh installation failed."
exit 1
fi

# 解压 acme.sh 配置信息
- name: Extract account files for acme.sh
shell: bash
run: |
echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
cat ~/.acme.sh/account.conf
env:
# Base64 编码的 acme.sh 配置信息
ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}

# 申请证书
- name: Issue SSL certificates
shell: bash
run: |
~/.acme.sh/acme.sh --issue \
-d "lan.jasper1024.com" -d *.lan.jasper1024.com \
--dns dns_cf --server letsencrypt --log
env:
CF_Token: ${{ secrets.CF_Token }}
CF_Account_ID: ${{ secrets.CF_Account_ID }}
CF_Zone_ID: ${{ secrets.CF_Zone_ID }}

# 导出证书
- name: Copy certificate to output paths
shell: bash
run: |
ACME_SH_TEMP_DIR="$(mktemp -d)"
ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"

# 不要忘记修改这里的 -d 参数值为上方的第一个域名
~/.acme.sh/acme.sh --install-cert -d "lan.jasper1024.com" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"

[[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
[[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")

rm -rf "$ACME_SH_TEMP_DIR"
env:
ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}

# 上传证书
- name: Push to GitHub
run: |
git config --global user.name "AcmeBot"
git config --global user.email "[email protected]"

cd "$CERTS_DIRECTORY"

git add "$FILE_FULLCHAIN" "$FILE_KEY"
git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
git push
env:
TZ: Asia/Shanghai
CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}

这样任何需要证书的地方 clone 这个仓库;

更好的方式: 交给自动化工具, 每次只传输更新后的证书;

Traefik

Traefik 配合容器简直起飞!!

  • 前排警告, 初次上手, 仅配置完反向代理就没再深究, 大概率目前配置并不是最佳实践.

首先创建一个 traefik 的网络 proxy

docker install traefik 的最简单示例

  • https://doc.traefik.io/traefik/getting-started/quick-start/

docker compose 下这两行可以让 http://lan.jasper1024.com/ -> http://lan.jasper1024.com/dashboard/

  • https://doc.traefik.io/traefik/operations/api/#configuration
1
2
3
labels:
- "traefik.http.routers.api.rule=Host(`lan.jasper1024.com`)"
- "traefik.http.routers.api.service=api@internal"

下面的配置可以正常 https 访问 https://lan.jasper1024.com/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ports:
- "443:443"
volumes:
- /opt/traefik/config:/etc/traefik
- /opt/traefik/acme:/opt/traefik/acme
command:
# 入口
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# file
- "--providers.file.directory=/etc/traefik"
- "--providers.file.watch=true"
labels:
- "traefik.enable=true"
# http router
- "traefik.http.routers.api.rule=Host(`lan.jasper1024.com`)"
- "traefik.http.routers.api.service=api@internal"
# https router
- "traefik.http.routers.api-secure.rule=Host(`lan.jasper1024.com`)"
- "traefik.http.routers.api-secure.entrypoints=websecure"
- "traefik.http.routers.api-secure.service=api@internal"
- "traefik.http.routers.api-secure.tls=true"

至此 不再需要修改 traefik, 只需要在其他容器内声明即可 以 gitea 为例

  • 一个网络下 proxy
  • labels 声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
gitea:
xxx
labels:
- "traefik.enable=true"
# HTTP配置
- "traefik.http.routers.gitea.rule=Host(`git.lan.jasper1024.com`)"
- "traefik.http.routers.gitea.entrypoints=web"
- "traefik.http.services.gitea.loadbalancer.server.port=3333"
# HTTPS配置
- "traefik.http.routers.gitea-secure.rule=Host(`git.lan.jasper1024.com`)"
- "traefik.http.routers.gitea-secure.entrypoints=websecure"
- "traefik.http.routers.gitea-secure.tls=true"
networks:
- proxy

networks:
proxy:
external: true