HanyanOS 部署手记
Introduction
A self-hosted infrastructure with seven public-facing domains demands robust TLS certificate management. HanyanOS routes traffic for chenyun.org, www.chenyun.org, n8n.chenyun.org, photo.chenyun.org, mail.chenyun.org, hanyan.chenyun.org, and rss.chenyun.org through a single N100 server. Each subdomain requires valid, trusted certificates.
This post covers the certificate automation pipeline built around acme.sh with AWS Route53 DNS-01 challenge, enabling wildcard (*.chenyun.org) certificate issuance and fully automated renewal — no open ports, no webroot conflicts, no manual intervention.
1 | ┌─────────────────────────────────────────────────────────────────┐ |
1. Why DNS-01 Challenge
HTTP-01 challenges require port 80 accessibility and per-domain webroot configuration — problematic for a server behind FRP tunnels where traffic routing can be asymmetrical. TLS-ALPN-01 requires direct port 443 on the domain. Both break when your VPS reverse-proxies to a home server.
DNS-01 challenge solves this:
- Wildcard support:
*.chenyun.orgcovers all subdomains in one cert - No open ports: Validation happens entirely via DNS TXT records
- Offline validation: Works even if the upstream VPS is restarting
- No webroot conflicts: Every service can serve HTTPS without knowing about ACME
The cost: you need API access to your DNS provider. For AWS Route53, that means an IAM user with route53:ChangeResourceRecordSets and route53:GetChange permissions.
2. acme.sh Installation & Account Setup
acme.sh is a pure-shell ACME client — no dependencies beyond curl, openssl, and cron. Install as a non-root user:
1 | curl https://get.acme.sh | sh |
This creates ~/.acme.sh/ with the client binary, DNS API plugins, and account configuration. No sudo needed for issuance — only the deploy step requires root (for writing to /etc/ssl/).
The account configuration stores the default ACME server and AWS credentials:
1 | ~/.acme.sh/account.conf |
Security note: AWS credentials are stored in plaintext in account.conf. This file is only readable by the michael user. For production environments, consider using AWS IAM Instance Profiles or a secrets manager — but for a single-user N100, file permissions suffice.
3. Wildcard Certificate Issuance
Issue a wildcard ECC certificate (P-256 / secp256r1):
1 | export AWS_ACCESS_KEY_ID='AKIA...' |
The dns_aws plugin:
- Creates a TXT record
_acme-challenge.chenyun.orgin Route53 - Polls until the record propagates (typically 10-60 seconds)
- Triggers Let’s Encrypt validation
- Removes the TXT record after validation
Result (stored in ~/.acme.sh/chenyun.org_ecc/):
1 | chenyun.org_ecc/ |
The domain config stores metadata including issue date, renewal deadlines, and API endpoints:
1 | Le_Domain='chenyun.org' |
4. Certificate Deployment
acme.sh stores certificates in ~/.acme.sh/<domain>_ecc/. For nginx to use them, they must be readable by the nginx worker process (usually www-data).
The HanyanOS deployment uses a two-layer strategy:
Layer 1: Copy certs to /etc/ssl/chenyun/ (root-owned, nginx-readable)
1 | install -o root -g root -m 644 \ |
Layer 2: Certbot-style symlinks for compatibility
1 | /etc/letsencrypt/live/chenyun.org/ |
Nginx configuration references the certbot-compatible path:
1 | ssl_certificate /etc/letsencrypt/live/chenyun.org/fullchain.pem; |
This design means you can swap the certificate source (acme.sh, certbot, manual CA) without touching nginx configuration — only the symlink target changes.
Important: Note the permission mismatch — fullchain.cer is 644 (world-readable), while chenyun.org.key is 600 (owner-only). The nginx worker must have CAP_DAC_OVERRIDE or be part of the ssl-cert group to read the key file.
5. The Second Certificate: hanyan.chenyun.org
A separate certificate exists for hanyan.chenyun.org — deployed to /tmp/ for specific services:
1 | Le_Domain='hanyan.chenyun.org' |
This is used for Agent-to-Agent TLS communication. The /tmp/ deploy path avoids permission issues when non-root processes (OpenClaw agents) need direct TLS access without reading /etc/ssl/.
6. Automation: Cron-Based Renewal
Let’s Encrypt certificates are valid for 90 days. acme.sh schedules renewal at 60 days (30 days before expiry):
1 | 56 20 * * * "/home/michael/.acme.sh"/acme.sh --cron --home "/home/michael/.acme.sh" > /dev/null |
Runs daily at 20:56. acme.sh checks each certificate’s Le_NextRenewTimeStr against the current date and only renews if within the renewal window. Renewal is idempotent — re-running before expiry is a no-op.
The renewal flow:
- Renew via DNS challenge (same
dns_awsplugin) - Overwrite certificate files in
~/.acme.sh/<domain>_ecc/ - Does not auto-deploy — the deploy step must be triggered manually or via
--renew-hook
Lesson learned: Initially, deployment was not part of the renewal hook. After the first automatic renewal, nginx was still serving the old certificate until the next manual deploy, causing a brief gap where browsers showed “Certificate Expires Soon” warnings. The fix: add a --renew-hook that copies to /etc/ssl/chenyun/ and reloads nginx:
1 | acme.sh --issue \ |
7. Nginx TLS Configuration
Each virtual host in the SNI proxy shares a common TLS configuration:
1 | ssl_certificate /etc/letsencrypt/live/chenyun.org/fullchain.pem; |
The ECDSA P-256 key (output of --keylength ec-256) enables TLS 1.3 with perfect forward secrecy, smaller handshake payloads than RSA, and CPU-efficient key exchange — ideal for an N100’s modest resources.
8. TLS Certificate Validation
Verify the deployed certificate:
1 | # View certificate details |
9. Security Considerations
| Concern | Mitigation |
|---|---|
| AWS key exposure | account.conf is user-readable only; restrict IAM to route53:* on the hosted zone |
| Key theft | Private key at 600 permissions; no remote backup |
| Certificate transparency | Automatic — Let’s Encrypt logs all certs to CT logs |
| OCSP stapling | Configure ssl_stapling on; in nginx for revocation checks |
| DNS propagation delays | Route53 is near-instant; dns_aws plugin polls with 10s intervals |
10. Lessons Learned
Renewal hooks are non-negotiable: Without a
--renew-hook, auto-renewal produces fresh certs that sit unused. Always include the deploy-and-reload step.Symlink indirection pays off: By pointing nginx at certbot-style paths and symlinking to the actual cert source, you can switch ACME clients, CAs, or deployment strategies without touching every virtual host config.
ECC over RSA for embedded: On the N100’s Intel N100 (Alder Lake-N, 4 cores), ECDSA P-256 handshake completes ~3× faster than RSA 2048. The cert file is also ~400 bytes smaller — marginal, but every byte counts on constrained systems.
Route53 rate limits: If you manage many domains, the
dns_awsplugin’s default polling can hit Route53 API rate limits. SetSAVED_AWS_DNS_SLOWRATE=1inaccount.confto add jitter.Test with staging first: Always issue against
--server https://acme-staging-v02.api.letsencrypt.org/directoryfirst. A misconfigured DNS challenge can bump into Let’s Encrypt’s 5-failures-per-hour rate limit.
Conclusion
The acme.sh + Route53 DNS-01 pipeline provides fully automated, zero-touch certificate management for all seven HanyanOS domains. The wildcard ECC certificate covers every subdomain, renews unattended, and the symlink-based deployment decouples the certificate source from the web server configuration.
For a self-hosted infrastructure behind FRP tunnels, DNS-01 challenge is the only practical choice — and with Route53’s API, the setup is both reliable and maintainable with about 20 lines of configuration and a single cron entry.
Part of the HanyanOS 部署手记 series. Next: TBD.