diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..f0c7345 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,132 @@ +# 操作筆記 + +README 是**現況快照**(會過期);這份是**做法**(重複用得到)。 + +## 網域架構:兩套,別搞混 + +| 網域 | DNS | 對外 IP | 意義 | +|---|---|---|---| +| `lotimmy.com` | Cloudflare **proxied**(橘雲) | `104.21.26.142` / `172.67.136.127` | 流量經 Cloudflare 再回源 | +| `timmy.edu.kg` | Cloudflare **DNS only**(灰雲) | `125.229.110.50`(直指自家 IP) | 流量直達,Cloudflare 只做解析 | + +Zone ID:`timmy.edu.kg` = `76ae4a4445b95c6e2a36708fcc095058` + +新子網域一律照該 zone 現有記錄的 proxied 狀態複製,不要混用。**沒有 wildcard 記錄**,每個子網域都要單獨加。 + +## 新增一個對外服務 + +順序不能顛倒 —— 沒有 DNS,Let's Encrypt 的 HTTP-01 challenge 一定失敗。 + +```bash +# 1. Cloudflare 加 A 記錄(proxied 依 zone 慣例) +curl -X POST -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \ + -d '{"type":"A","name":"","content":"125.229.110.50","ttl":1,"proxied":false}' \ + "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records" + +# 2. 等生效 +dig +short .timmy.edu.kg @1.1.1.1 + +# 3. 取 NPM token +TOK=$(curl -s -X POST -H "Content-Type: application/json" \ + -d '{"identity":"","secret":""}' \ + http://192.168.42.124:81/api/tokens | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") + +# 4. 先簽憑證(回傳的 id 下一步要用) +curl -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \ + -d '{"provider":"letsencrypt","nice_name":"","domain_names":[""],"meta":{"dns_challenge":false}}' \ + http://192.168.42.124:81/api/nginx/certificates + +# 5. 再建 proxy host +curl -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" -d '{ + "domain_names":[""],"forward_scheme":"http","forward_host":"","forward_port":, + "certificate_id":,"ssl_forced":true,"http2_support":true,"hsts_enabled":false, + "allow_websocket_upgrade":true,"block_exploits":true,"caching_enabled":false, + "access_list_id":0,"advanced_config":"","locations":[],"meta":{}}' \ + http://192.168.42.124:81/api/nginx/proxy-hosts +``` + +驗證:`curl -o /dev/null -w "%{http_code}"` 打 https 應得 200、http 應得 301。 + +## NPM 2.15.1 API 的坑 + +- **`meta` 只吃這些欄位**:`certificate`、`certificate_key`、`dns_challenge`、`dns_provider`、`dns_provider_credentials`、`letsencrypt_certificate`、`propagation_seconds`、`key_type`。 + 舊教學常見的 `letsencrypt_email` / `letsencrypt_agree` **會被拒**(`data/meta must NOT have additional properties`),email 改由全域設定帶。 +- schema 就在容器裡,不確定就直接查,不要猜: + ```bash + docker exec nginxproxymanager cat /app/schema/paths/nginx/certificates/post.json + docker exec nginxproxymanager cat /app/schema/components/certificate-object.json + ``` +- 目前 40 張憑證**全部用 HTTP-01**,DB 裡沒有任何 DNS provider 憑證。 +- 查「有幾張用 DNS challenge」不要用 `meta like '%dns_challenge%true%'` —— 會誤中 `"dns_challenge":false,"nginx_online":true`。要寫 `meta like '%"dns_challenge":true%'`。 + +## 刪除:直接操 DB 的正確步驟 + +NPM 不會監看 DB,光改 DB 不會生效,**conf 檔和 nginx reload 要自己補**。 + +```bash +cd /opt/nginx-proxy-manager/data +cp database.sqlite database.sqlite.bak-$(date +%F) # 先備份,非選配 + +# 刪之前一定要查引用,確認不是使用中的服務 +sqlite3 -separator '|' database.sqlite \ + "select id,domain_names,is_deleted,enabled from proxy_host where certificate_id=;" + +sqlite3 database.sqlite "update proxy_host set is_deleted=1 where id=; + update certificate set is_deleted=1 where id=;" +rm -f nginx/proxy_host/.conf # 少這步 → 網站還活著 + +docker exec nginxproxymanager certbot delete --cert-name npm- --non-interactive \ + --config-dir /etc/letsencrypt --work-dir /tmp/letsencrypt-lib --logs-dir /tmp/letsencrypt-log + +docker exec nginxproxymanager nginx -t && docker exec nginxproxymanager nginx -s reload +``` + +慣例:憑證 id 為 `N` ↔ `letsencrypt/{live,archive}/npm-N` 與 `renewal/npm-N.conf`。 + +驗證要帶 Host header 打 **port 80**,不要打 443 —— 443 沒給 SNI 會 `curl: (35)`,看起來像壞掉其實是測法錯: + +```bash +curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1 -H "Host: " # 已刪 → 404 +``` + +每次都要有**對照組**(拿 `web.lotimmy.com` 之類還活著的比),否則分不出「刪對了」和「整台 nginx 掛了」。 + +找孤兒 conf: + +```bash +for f in nginx/proxy_host/*.conf; do id=$(basename $f .conf) + [ "$(sqlite3 database.sqlite "select count(*) from proxy_host where id=$id and is_deleted=0;")" = 1 ] \ + || echo "孤兒: $f"; done +``` + +## Cloudflare API + +`cfat_` 開頭的 token **打 `/user/tokens/verify` 會回 `Invalid API Token`,但 token 是好的**。別因為這個結論說 token 失效 —— 直接打實際端點測: + +```bash +curl -H "Authorization: Bearer $CF_TOKEN" \ + "https://api.cloudflare.com/client/v4/zones?name=timmy.edu.kg" +``` + +## Git 推送到自架 Gitea + +`git push http://user:token@host/...` 會把**含 token 的 URL 寫進 `.git/config`** 的 upstream。推完要清: + +```bash +git config --unset branch.main.remote +git config --unset branch.main.merge +git fetch origin && git branch --set-upstream-to=origin/main main +grep -rn "" .git/ && echo "⚠️ 有殘留" +``` + +`origin` 本身存不含帳密的 URL。 + +## 反向代理後別忘了改應用端 base URL + +代理設好只是流量進得來;應用自己產生的連結(clone URL、通知信、OAuth callback)還是用它自己的設定。Gitea 是 `app.ini` 的 `[server] ROOT_URL`,改完要重啟。**這是目前未完成項**。 + +## 雜項 + +- `client_max_body_size` 全域已是 `2000m`(`/etc/nginx/nginx.conf:39`),git push 大 repo 不會被擋,不用另外寫 advanced config。 +- `certbot delete` 只清該 lineage。DB 早就軟刪除、檔案還留著的舊 lineage(如 `npm-4`)不會自己消失。 +- `dead_host` 的 `*.lotimmy.com` 會接住所有未定義子網域,所以刪掉 proxy host 後是 404 而不是連到別的站。 diff --git a/README.md b/README.md index 53c40bd..f80caf5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - **狀態**:容器 `nginxproxymanager` 已連續運行 5 週,佔用 80 / 81 / 443 > 本文件為 2026-08-01 的現況快照,資料來自主機上的 `docker-compose.yml` 與 `data/database.sqlite`。 +> 操作方式(新增服務、刪除、API 用法、踩過的坑)見 [NOTES.md](NOTES.md)。 ## 部署