[teamai] Push 1 resource(s) from root

This commit is contained in:
2026-09-10 16:51:18 +08:00
parent f3388c50e2
commit f8980810ee
2 changed files with 74 additions and 0 deletions
@@ -0,0 +1 @@
root
+73
View File
@@ -0,0 +1,73 @@
---
name: "expose-service-subdomain"
description: "Expose an internal K3s/container service to a public *.yoresee.cc URL via the edge nginx reverse proxy (DNS→certbot→site rewrite→base_url→verify), and harden a static download site (autoindex off / robots.txt)."
---
# Expose a service on the edge nginx via subdomain
Push an internal K3s/container service to a public `*.yoresee.cc` URL through the edge nginx on 十堰电信4c8g (125.208.22.116). Targets: a K3s ClusterIP (use svc IP:port) or a container on 襄阳2c4g/other node (use node IP:port).
**If the service is not running yet** (no image/container/pod), obtain and run it first — see `deploy-docker-service`.
## Steps
1. **Confirm DNS first.** `getent hosts <domain>` must return the edge IP `125.208.22.116`. If it does not, stop and ask the user to add the A record — do not proceed until it resolves.
2. **Sign the cert WITHOUT rewriting config.** Reuse the existing Let's Encrypt account:
```bash
certbot certonly --nginx -d <domain> --non-interactive --agree-tos --keep-until-expiring
```
⚠️ Only `certonly` is safe. Do NOT run plain `certbot --nginx` — its auto-configuer rewrites the reverse-proxy site (breaks the 80→443 redirect and the proxy_pass). Preserve the site file by hand.
3. **Back up the current site config, then rewrite it in the edge pattern** (80 redirect + 443 http2 proxy):
```nginx
server {
listen 80; listen [::]:80; server_name <domain>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2; listen [::]:443 ssl http2; server_name <domain>;
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
location / {
proxy_pass http://<TARGET_IP>:<TARGET_PORT>;
proxy_http_version 1.1;
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;
}
}
```
TARGET_IP: for a K3s service use its ClusterIP (`kubectl get svc -A`), for a container on another node use that node reachable IP. For a web UI add `client_max_body_size` / long timeouts as the service needs.
4. **Match any base_url to the final URL.** If the service supports a `server.base_url` (e.g. searxng) and you serve it at the root domain, set `base_url: "https://<domain>/"` (no subpath). If an earlier step had mounted it under a subpath (`/searx/`), revert that — remove the trailing subpath from base_url AND delete the `location /subpath/` block in nginx. Mismatched base_url leaves pages loading but static assets pointing at the wrong path.
5. **Reload and verify each domain.** `nginx -t` then `systemctl reload nginx`. Then:
```bash
curl -s -m 12 -o /tmp/x.html -w "HTTP %{http_code}\n" https://<domain>/
grep -o '<title>[^<]*' /tmp/x.html | head -1
grep -o 'src="[^"]*"' /tmp/x.html | head -2
```
Confirm the `src` paths have no leftover `/subpath/` prefix (should be root `/static/...` for a root-mounted service). Run a real query through the public URL if it is search-like. If the service requires auth, pull the real credential from the K8s Secret (`kubectl get secret <name> -o jsonpath='{.data.<KEY>}' | base64 -d`) and use it — never hand-type a placeholder, which yields a spurious 401/`bad credentials` and a wasted cycle.
6. **Record the mapping** in the workspace memory so future work does not collide with the domain.
## Hardening a static download site (optional)
`downloads.yoresee.cc` serves `/var/www/downloads` from the edge host with `autoindex on`, so its root listed every file and AI crawlers (GPTBot on Azure, ClaudeBot on AWS — check `/var/log/nginx/access.log` for the UA) walked the listing. To stop enumeration:
1. Back up the site file to `/root/backup/`, then turn the listing off (`autoindex on` → `autoindex off`), `nginx -t`, `systemctl reload nginx`. This is the real lock: the root then returns 403.
2. Write `robots.txt` into the docroot: one `User-agent: <bot>` + `Disallow: /` block per crawler, then a permissive `User-agent: *`; `chown` it to the docroot owner. Advisory only — it does not stop an ignoring crawler.
3. Verify over loopback with the Host header (no DNS round trip): `curl -s -k https://127.0.0.1/ -H "Host: <domain>"` → 403; `curl -s -k -r 0-1000 https://127.0.0.1/<file> -H "Host: <domain>"` → 206 for a real file, so existing direct links (e.g. shared into Feishu) keep working.
## Pitfalls
- `certbot --nginx` (auto) destroys the reverse-proxy site — always use `certonly --nginx` and write the site file yourself.
- base_url must equal the public origin (path included) or assets 404 / pages load broken.
- The edge nginx site files live in `/etc/nginx/sites-enabled/`; back up (to `/root/backup/`) before rewriting so the switch is reversible.
- Some images ship with no default command (e.g. `binwiederhier/ntfy:v2`): the entrypoint prints help and exits, so the pod CrashLoopBackOffs right after a successful pull. Fix in the Deployment by adding the server subcommand explicitly, e.g. `args: ["serve"]` (ntfy) or the equivalent `serve`/`server` subcommand for that image. Verify with `kubectl logs -l app=<name>`: help output = missing command; `Listening on` = fixed.
- A multi-container app's **own** front-end nginx (`location /api/ { set $api_upstream http://<svc>:<port>; proxy_pass $api_upstream; }`) fails with 502 when `<svc>` is a short name: nginx's resolver does NOT apply the Kubernetes search domain (unlike `nslookup`/libc), so coredns returns NXDOMAIN for single-label names. Use the fully-qualified service DNS in the app's nginx.conf and rebuild/redeploy that web image. The FQDN is `<svc>.<namespace>.svc.cluster.local:<port>` — the namespace segment is a literal part of the name, so a service in `default` is `api.default.svc.cluster.local:8080` but the same service deployed in a non-default namespace (e.g. `test`) is `api.test.svc.cluster.local:8080`, NOT `default`. Copying the `default` example when the service lives elsewhere will 502. Short names only work for the edge nginx's `proxy_pass` because that resolves at startup via libc, not the resolver.
- `certbot certonly --nginx` fails if the site config is written with `ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem` BEFORE the cert exists — certbot runs `nginx -t`, which errors "cannot load certificate". Two ways out: (a) create the site file WITHOUT the ssl_certificate lines, sign the cert, then add them; or (b) the simpler route — sign first with webroot `certbot certonly --webroot -w /var/www/html -d <domain> --non-interactive --agree-tos --keep-until-expiring`, which needs no live config, then write the full site. If `--nginx` errors with a missing-cert message, switch to webroot.
- **Do not expose a public download/serve on bare port 80 plaintext HTTP — ISP DPI truncates it.** Serving a static file from `http://125.208.22.116:80` (no domain/HTTPS) works locally but the public path returns truncated data followed by a synthetic 404. Key diagnostic: the nginx access log records `200` with a large `body_bytes_sent` (e.g. 231393) while `curl` only downloaded a few KB or got a 404, and a second egress (aliyun) sees the same 404. That mismatch (log says 200, client got 404/partial) is DPI, not a config bug. Fix: serve via HTTPS on 443 for a real domain — the same file over HTTPS downloads fully. For any public serve/download job, go straight to domain + certbot (webroot) + 443 and verify over HTTPS; do not burn cycles testing bare-IP:80.