feat: add certmagic for automatic TLS certificate management
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m28s
All checks were successful
Docker Build and Push / build-and-push (push) Successful in 3m28s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ id_rsa*
|
|||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
tmp
|
tmp
|
||||||
|
certs
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -26,6 +26,10 @@ The following environment variables can be configured in the `.env` file:
|
|||||||
| `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No |
|
| `TLS_REDIRECT` | Redirect HTTP to HTTPS | `false` | No |
|
||||||
| `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No |
|
| `CERT_LOC` | Path to TLS certificate | `certs/cert.pem` | No |
|
||||||
| `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No |
|
| `KEY_LOC` | Path to TLS private key | `certs/privkey.pem` | No |
|
||||||
|
| `CERT_STORAGE_PATH` | Path for CertMagic certificate storage | `certs/certmagic` | No |
|
||||||
|
| `ACME_EMAIL` | Email for Let's Encrypt registration | `admin@<DOMAIN>` | No |
|
||||||
|
| `CF_API_TOKEN` | Cloudflare API token for DNS-01 challenge | - | Yes (if auto-cert) |
|
||||||
|
| `ACME_STAGING` | Use Let's Encrypt staging server | `false` | No |
|
||||||
| `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No |
|
| `SSH_PRIVATE_KEY` | Path to SSH private key (auto-generated if missing) | `certs/id_rsa` | No |
|
||||||
| `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No |
|
| `CORS_LIST` | Comma-separated list of allowed CORS origins | - | No |
|
||||||
| `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No |
|
| `ALLOWED_PORTS` | Port range for TCP tunnels (e.g., 40000-41000) | `40000-41000` | No |
|
||||||
@@ -35,6 +39,36 @@ The following environment variables can be configured in the `.env` file:
|
|||||||
|
|
||||||
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
|
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
|
||||||
|
|
||||||
|
### Automatic TLS Certificate Management
|
||||||
|
|
||||||
|
The server supports automatic TLS certificate generation and renewal using [CertMagic](https://github.com/caddyserver/certmagic) with Cloudflare DNS-01 challenge. This is required for wildcard certificate support (`*.yourdomain.com`).
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. If user-provided certificates (`CERT_LOC`, `KEY_LOC`) exist and cover both `DOMAIN` and `*.DOMAIN`, they will be used
|
||||||
|
2. If certificates are missing, expired, expiring within 30 days, or don't cover the required domains, CertMagic will automatically obtain new certificates from Let's Encrypt
|
||||||
|
3. Certificates are automatically renewed before expiration
|
||||||
|
4. User-provided certificates support hot-reload (changes detected every 30 seconds)
|
||||||
|
|
||||||
|
**Cloudflare API Token Setup:**
|
||||||
|
|
||||||
|
To use automatic certificate generation, you need a Cloudflare API token with the following permissions:
|
||||||
|
|
||||||
|
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens)
|
||||||
|
2. Click "Create Token"
|
||||||
|
3. Use "Create Custom Token" with these permissions:
|
||||||
|
- **Zone → Zone → Read** (for all zones or specific zone)
|
||||||
|
- **Zone → DNS → Edit** (for all zones or specific zone)
|
||||||
|
4. Copy the token and set it as `CF_API_TOKEN` environment variable
|
||||||
|
|
||||||
|
**Example configuration for automatic certificates:**
|
||||||
|
```env
|
||||||
|
DOMAIN=example.com
|
||||||
|
TLS_ENABLED=true
|
||||||
|
CF_API_TOKEN=your_cloudflare_api_token_here
|
||||||
|
ACME_EMAIL=admin@example.com
|
||||||
|
# ACME_STAGING=true # Uncomment for testing to avoid rate limits
|
||||||
|
```
|
||||||
|
|
||||||
### SSH Key Auto-Generation
|
### SSH Key Auto-Generation
|
||||||
|
|
||||||
If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys.
|
If the SSH private key specified in `SSH_PRIVATE_KEY` doesn't exist, the application will automatically generate a new 4096-bit RSA key pair at the specified location. This makes it easier to get started without manually creating SSH keys.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEWzCCA0OgAwIBAgIUDYTdEDHwVznxV/qnn0/WVlHNMZAwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwgZYxCzAJBgNVBAYTAklEMQ8wDQYDVQQIDAZCYW50ZW4xGjAYBgNVBAcMEVRh
|
|
||||||
bmdlcmFuZyBTZWxhdGFuMRIwEAYDVQQKDAlGb3NzeSBMVFMxDjAMBgNVBAsMBUZv
|
|
||||||
c3N5MRQwEgYDVQQDDAtmb3NzeS5teS5pZDEgMB4GCSqGSIb3DQEJARYRYmFnYXNA
|
|
||||||
Zm9zc3kubXkuaWQwHhcNMjUwMjA3MTYyMTU1WhcNMjYwMjA3MTYyMTU1WjCBljEL
|
|
||||||
MAkGA1UEBhMCSUQxDzANBgNVBAgMBkJhbnRlbjEaMBgGA1UEBwwRVGFuZ2VyYW5n
|
|
||||||
IFNlbGF0YW4xEjAQBgNVBAoMCUZvc3N5IExUUzEOMAwGA1UECwwFRm9zc3kxFDAS
|
|
||||||
BgNVBAMMC2Zvc3N5Lm15LmlkMSAwHgYJKoZIhvcNAQkBFhFiYWdhc0Bmb3NzeS5t
|
|
||||||
eS5pZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMU3pCA0eP+VR2CA
|
|
||||||
+p3p0BgKw33xCKQmQx52jnqNbvwW4NMMDc3mS6a+wb6QB6v0WLGMI1g22TtJPatx
|
|
||||||
7dcy4PCSOCV9xJ2Yfq5HlQgDHYoyE+juy4/pGlMjo45thJ0yI8zOSzaz2esosP22
|
|
||||||
XkfFwj7oVMXXPIY6UovcAlGU+DFtwVrNa76/esUwJs+7M3tBubjkpcal0wXR+SIX
|
|
||||||
3fmw5v0YzKV8qLGMGOvX6+OyLQCx4r9gB0d+WOrT6EfrPfAzo07NKjzWG9aWl1rk
|
|
||||||
Q+h0i38hke2MTFxId7za57L+NlXRjLo3ESbBF0hjYquTQOG3jx/UvWs+I1NEfsdu
|
|
||||||
vj8beiMCAwEAAaOBnjCBmzAfBgNVHSMEGDAWgBT6nj69+I+GSdgScjfOqnVFzX7G
|
|
||||||
aTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAs
|
|
||||||
BgNVHREEJTAjgglsb2NhbGhvc3SCCTEyNy4wLjAuMYILKi5sb2NhbGhvc3QwHQYD
|
|
||||||
VR0OBBYEFD9ci20pAUTRLVWxvVXjfE2GKwSAMA0GCSqGSIb3DQEBCwUAA4IBAQAT
|
|
||||||
rkjU+GzQy9B3/nd/79N7ozK80ygzmnRnj3ou/bbqUHOIYQQgKM1cuN6zh57ovRh/
|
|
||||||
u45s6pZUOUVN59POFUCvqUiOgsDkY/auXGbKtzzqzoZABuvm85ySd6zurOOx8tA5
|
|
||||||
e7ArX1ppy3LgzSb+cXANvzYC9bCwp70w2YylMFhwHBAp5TXRVqsqG6jujD8GMLoS
|
|
||||||
zDSDx8M6o8gqWmPQve7Saim9mgLJUvvCYBzvowuNjzZrJfAeGoIfLV127xCQiylm
|
|
||||||
fzUQ1Ac6udldWm9scA32nteSQMWg2d1nW3RG1nRondp13WUkgGQ490/c97D3MKLt
|
|
||||||
D1HB5dLIYkRVHVhCKHT1
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDjDCCAnSgAwIBAgIUPpuvw4ZdpnDBbwOZBt2ALfZykuYwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwPDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u
|
|
||||||
ZG9uMQswCQYDVQQLDAJIUTAeFw0yNDExMTkxNjQxNTNaFw0zNDExMTcxNjQxNTNa
|
|
||||||
MDwxCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRv
|
|
||||||
bjELMAkGA1UECwwCSFEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDD
|
|
||||||
L/aeaBzgkYxDNiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5E
|
|
||||||
sHptIpu5/uTBFaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgA
|
|
||||||
HKryOwMcB5DheBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9r
|
|
||||||
WSJllNHOFk+RH17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t
|
|
||||||
8Ik74GeNrM6f+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIk
|
|
||||||
tOot0YiZGQAoOedtGSEnAgMBAAGjgYUwgYIwCQYDVR0TBAIwADALBgNVHQ8EBAMC
|
|
||||||
BeAwSQYDVR0RBEIwQIIQbG9jYWxob3N0LmRpcmVjdIISKi5sb2NhbGhvc3QuZGly
|
|
||||||
ZWN0ghhTUy5jZXJ0LmxvY2FsaG9zdC5kaXJlY3QwHQYDVR0OBBYEFKBVeirQGZ4D
|
|
||||||
4AKVPd7LGfCF1wEZMA0GCSqGSIb3DQEBCwUAA4IBAQCRpvsc5DrQBo8yATmUS0OK
|
|
||||||
xfUXfZR28u3xYY+qMHi+ngIVT2TKJ1yoBJezV6WwQLkcGdWacULvPYt3jCFUtaP7
|
|
||||||
+hzfs5y1FssFsXDx+r3pQxYyE6BX3BhlrJPJhLRyG1siTTgN439Qu40/TsTzNgAT
|
|
||||||
A9GbAro+W6+qA4H92mBlyfQEOBossID/Kk95uvDnQguUOp0ZBLgFNRfE6Ra9+yC+
|
|
||||||
ufAOksYDrisPE6kZId0Ra4Ln/GmrIXKjjmLCitq2q2REC/70JSCnaJcBYeThSJLZ
|
|
||||||
AZ24AF+JteakSJ8FEgRGxvSu0wdZfnMobNoelsjai1p5l5mCTiD8GH9sQCkslnp3
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDDL/aeaBzgkYxD
|
|
||||||
NiQq7+nt6tKDfGnPDfBXunJlr1xbZfIVpJeDqwrNLWaZ0gtHci5EsHptIpu5/uTB
|
|
||||||
FaFyZVH604etY/YHIsff7BT2y32OobJYiKy2lvAI3/IDR4TGeDgAHKryOwMcB5Dh
|
|
||||||
eBVdeggxj36m8OjxhVADaiKp7BNXE2eqUqk8f2QpwqLQYe9UaU9rWSJllNHOFk+R
|
|
||||||
H17YBDiyyQ8CD1Vf5HcVSmPItWOMQytHcSgy0DHVXQCde86mky8t8Ik74GeNrM6f
|
|
||||||
+vWR4OfQ8dU2WSyTUE4c7czagkToheMX5fbbzWJkJcd2SD9wvyIktOot0YiZGQAo
|
|
||||||
OedtGSEnAgMBAAECggEASXibb2MnQ4zl7ELL+HGYb5sNpLrHJU5M4ujmuMn6jNjh
|
|
||||||
+C2dbs2KYlMtpMcAweMD8Y0weDYnwiplNx0KSYJECpNnJehTqrn33J0EAyXz3CWX
|
|
||||||
eWXxBUXpkp2hfoSEQSTth3VDD60Q7ZMXgRdvi2EtBmLKPNLADHGu/aoM5ENdwE9/
|
|
||||||
E80X68E+MT2czOY/sEI/w2Tf/S2ZVOHRvFOsmvTLFlbiElWG8pmguajpJwdae7uX
|
|
||||||
c5VD87b0oYicSUvaHe6xOCCyyeBVq06sWk+vh6Tzrw6K/B+q0SYvzXdrdsJxbzUD
|
|
||||||
PvhVi9rf9AC1Ncb6lFOP2ZjGfqYQvfgGXKqaVxXWQQKBgQD8frATynMSeR15oERa
|
|
||||||
+Maa7r3GlwWV3tkUblX7/FxBo9UhAsivnWZprccZR43YPowbWNeU3AQppf/YDxmx
|
|
||||||
70/RVTCMjXPGyspSS2iFtcp+K2KnIZA4BuAG/s1EBrAUW2KrtaTaXgYu9usgb+Fq
|
|
||||||
dJUEBDWrL59XXtQUwSM1laH0BwKBgQDF5Z31VY96xOS7flmolq8Ag+frSMH0G4np
|
|
||||||
3nUnTlUkgpFXE5FkmUccbYZv9QialAVriBBUNANPDBQH4PRrNnX8Z6B2HJbpAy30
|
|
||||||
II9jPMTNKnXT3RKFcJCamNkOQhT0sBAwm4gTsx+7gbpdZxinxH0Cr/08sfU4lbee
|
|
||||||
EtMV/J1h4QKBgC4MLLBvS20jCW0U/WJZ3F6FC7cb87jRW2WOeb/q1ihiaIwMpezh
|
|
||||||
F7xOJPFHS2cUgRi7qxVKyreNvor4tgbtTfEvSBtZ8LNgaGV5uyYncTZxUxyH0nVl
|
|
||||||
S5X7AhRV4+bSg7ws9FOesiH+hgL0ZHe1qzeATQlbNgQJF0RxtKohD9ghAoGAcM0N
|
|
||||||
WIZInoYUivreSEZ7wiNt0qNKSsZXukLfLGRuC72Q8r1opprn+cBEXRSirtmorT6F
|
|
||||||
cDmlmS0dTdBgAaytXA4FXM23B2KUkw7sLHi7BOcq+nSM1hrvke+F6aapI0AoOkyt
|
|
||||||
J+12LP8pJ4xYdWh+iUWfZzVYvcQ5QZUhVOsFGoECgYA9llKmc/cFaXrCr97g7ls8
|
|
||||||
ZWW1kQKLawAv6+90dwECJl+zpyQN/TURyEfz6oFJDbNEL0xAAcm9thDah177Tq95
|
|
||||||
pcHbVn/pAI4h4CLzM2ExSe8Ybvy6iPZaBiCVXq3ms1PK0EDyYLH1p3FZqm+UStZh
|
|
||||||
/6fYspyivrQEivRK3jGuWA==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFN6QgNHj/lUdg
|
|
||||||
gPqd6dAYCsN98QikJkMedo56jW78FuDTDA3N5kumvsG+kAer9FixjCNYNtk7ST2r
|
|
||||||
ce3XMuDwkjglfcSdmH6uR5UIAx2KMhPo7suP6RpTI6OObYSdMiPMzks2s9nrKLD9
|
|
||||||
tl5HxcI+6FTF1zyGOlKL3AJRlPgxbcFazWu+v3rFMCbPuzN7Qbm45KXGpdMF0fki
|
|
||||||
F935sOb9GMylfKixjBjr1+vjsi0AseK/YAdHfljq0+hH6z3wM6NOzSo81hvWlpda
|
|
||||||
5EPodIt/IZHtjExcSHe82uey/jZV0Yy6NxEmwRdIY2Krk0Dht48f1L1rPiNTRH7H
|
|
||||||
br4/G3ojAgMBAAECggEAATiJ23ZhS1++squ87jsgAfR+TYP8Kpv408u/43Ig5KgC
|
|
||||||
ZnwPUWqvP2e0TLwyhMKwXxHMt2nITxQlSnyCXU/5nk07BYxkqhiwEni4Xo86YK+H
|
|
||||||
rQXEnKFaSHdF6dNNIo+VZiark4adS9XgJp0fsn0LnON7GhBt72MvPW6auxH1HJIC
|
|
||||||
rcjnzJu/KzTVrId9QNsEDl9cNRlHXuPSfohdq2o39PBKGDeDEDTvP9wHtasEF6oe
|
|
||||||
uj1OH5fAxiXptT0Ln0Y8QeFe8R8Odul5mUXCgMOkKmHZPDxTq9ldCuPjrz15JfnS
|
|
||||||
T3MecaWdvChpIP2WybjLstJy9qXTl0fhRkpJWpaVbQKBgQDu9puq64vZn0jak7ns
|
|
||||||
bFbKPemFw7tiiwwnqyzqCTRddw1vETJN/MgpRWvc7d1Cvzs2+Z2kn0Cgpdw/30Ej
|
|
||||||
VRHiE/d1rj+u33F6vGBUBccRue1t0UdFMFF/YnlNDRRnljQgx4N3mezzNE2RikR8
|
|
||||||
jVp+jvWTSTgf3y4Hip1ffCSNtQKBgQDTRx1huSzUOed/36YLdnxP+AlV3L+c/EKU
|
|
||||||
GItKZk2bqRlqgs6wcsNlVjveb9o9j6M5pg/lh5HaqJyhj8DYVnEdmXfcQJiUfIPh
|
|
||||||
5802asIXQVBmL9rTMR34wsr/lzzK3k0KORqFcUdf2fDI/UQNpVUJvWQQY8fl2sVo
|
|
||||||
zmp88JAPdwKBgQCLC6fsvn5ztMF5nffTX/7oUzosgYXpgyshcfMCgzSbJgkFFaaF
|
|
||||||
xo7ZpPFsbmQO0KMuC/T0s02xrJEKAWgvnPJ48FFPgoK/yHiJiE8s1OfOordK7Tlh
|
|
||||||
QwpI6w3WDcRPuhC++hi/YSuFIGv6QdA0ATQk7B5tA2/K69wmuztzMhM6+QKBgH5E
|
|
||||||
LwQbRfZj0L20bKjHHA4y32loLz/j5upZLM2/DDyuN9lW6a28OJiUi90pHdXSxSsL
|
|
||||||
2s5DUmDKiiloH0lrh9i3wlFobYe4Tp0xCoyuCucZCrK3gODcptvnlqhfu15GsuYc
|
|
||||||
MIR1qcFYH7YO3qAFIiha/rVo3Ku7LmWvjyayInaLAoGAZ/dELEAcyhImf1HvtmBT
|
|
||||||
qNg4uI6t/2fvHdoAeQkkGjEDWFTGEaMp0cJEbETPD68TwNh5dL/xUJibTwMbK+m5
|
|
||||||
rdIA2oTZMkFtWubIvMCrD/J6E+8Pzl+eK+0C0axO/I29S4veORNGWvCtqdFICDgZ
|
|
||||||
CluJ0NZFeMH8g8tgfI9HnHc=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
20
go.mod
20
go.mod
@@ -7,4 +7,22 @@ require (
|
|||||||
golang.org/x/crypto v0.45.0
|
golang.org/x/crypto v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.38.0 // indirect
|
require (
|
||||||
|
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/libdns/cloudflare v0.2.2 // indirect
|
||||||
|
github.com/libdns/libdns v1.1.1 // indirect
|
||||||
|
github.com/mholt/acmez/v3 v3.1.3 // indirect
|
||||||
|
github.com/miekg/dns v1.1.68 // indirect
|
||||||
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
|
golang.org/x/net v0.47.0 // indirect
|
||||||
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
32
go.sum
32
go.sum
@@ -1,9 +1,37 @@
|
|||||||
|
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
||||||
|
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||||
|
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||||
|
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||||
|
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||||
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
|
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||||
|
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||||
|
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||||
|
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
@@ -11,3 +39,7 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewHTTPSServer() error {
|
func NewHTTPSServer() error {
|
||||||
cert, err := tls.LoadX509KeyPair(utils.Getenv("CERT_LOC", "certs/cert.pem"), utils.Getenv("KEY_LOC", "certs/privkey.pem"))
|
domain := utils.Getenv("DOMAIN", "localhost")
|
||||||
|
|
||||||
|
tlsConfig, err := NewTLSConfig(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to initialize TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &tls.Config{Certificates: []tls.Certificate{cert}}
|
ln, err := tls.Listen("tcp", ":443", tlsConfig)
|
||||||
ln, err := tls.Listen("tcp", ":443", config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
312
server/tls.go
Normal file
312
server/tls.go
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"tunnel_pls/utils"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLSManager struct {
|
||||||
|
domain string
|
||||||
|
certPath string
|
||||||
|
keyPath string
|
||||||
|
storagePath string
|
||||||
|
|
||||||
|
userCert *tls.Certificate
|
||||||
|
userCertMu sync.RWMutex
|
||||||
|
|
||||||
|
magic *certmagic.Config
|
||||||
|
|
||||||
|
useCertMagic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsManager *TLSManager
|
||||||
|
var tlsManagerOnce sync.Once
|
||||||
|
|
||||||
|
func NewTLSConfig(domain string) (*tls.Config, error) {
|
||||||
|
var initErr error
|
||||||
|
|
||||||
|
tlsManagerOnce.Do(func() {
|
||||||
|
certPath := utils.Getenv("CERT_LOC", "certs/cert.pem")
|
||||||
|
keyPath := utils.Getenv("KEY_LOC", "certs/privkey.pem")
|
||||||
|
storagePath := utils.Getenv("CERT_STORAGE_PATH", "certs/certmagic")
|
||||||
|
|
||||||
|
tm := &TLSManager{
|
||||||
|
domain: domain,
|
||||||
|
certPath: certPath,
|
||||||
|
keyPath: keyPath,
|
||||||
|
storagePath: storagePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tm.userCertsExistAndValid() {
|
||||||
|
log.Printf("Using user-provided certificates from %s and %s", certPath, keyPath)
|
||||||
|
if err := tm.loadUserCerts(); err != nil {
|
||||||
|
initErr = fmt.Errorf("failed to load user certificates: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tm.useCertMagic = false
|
||||||
|
tm.startCertWatcher()
|
||||||
|
} else {
|
||||||
|
if !isACMEConfigComplete() {
|
||||||
|
log.Printf("User certificates missing or invalid, and ACME configuration is incomplete")
|
||||||
|
log.Printf("To enable automatic certificate generation, set CF_API_TOKEN environment variable")
|
||||||
|
initErr = fmt.Errorf("no valid certificates found and ACME configuration is incomplete (CF_API_TOKEN is required)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("User certificates missing or don't cover %s and *.%s, using CertMagic", domain, domain)
|
||||||
|
if err := tm.initCertMagic(); err != nil {
|
||||||
|
initErr = fmt.Errorf("failed to initialize CertMagic: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tm.useCertMagic = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsManager = tm
|
||||||
|
})
|
||||||
|
|
||||||
|
if initErr != nil {
|
||||||
|
return nil, initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsManager.getTLSConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isACMEConfigComplete() bool {
|
||||||
|
cfAPIToken := utils.Getenv("CF_API_TOKEN", "")
|
||||||
|
return cfAPIToken != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) userCertsExistAndValid() bool {
|
||||||
|
if _, err := os.Stat(tm.certPath); os.IsNotExist(err) {
|
||||||
|
log.Printf("Certificate file not found: %s", tm.certPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(tm.keyPath); os.IsNotExist(err) {
|
||||||
|
log.Printf("Key file not found: %s", tm.keyPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateCertDomains(tm.certPath, tm.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateCertDomains(certPath, domain string) bool {
|
||||||
|
certPEM, err := os.ReadFile(certPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to read certificate: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(certPEM)
|
||||||
|
if block == nil {
|
||||||
|
log.Printf("Failed to decode PEM block from certificate")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to parse certificate: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(cert.NotAfter) {
|
||||||
|
log.Printf("Certificate has expired (NotAfter: %v)", cert.NotAfter)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) {
|
||||||
|
log.Printf("Certificate expiring soon (NotAfter: %v), will use CertMagic for renewal", cert.NotAfter)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var certDomains []string
|
||||||
|
if cert.Subject.CommonName != "" {
|
||||||
|
certDomains = append(certDomains, cert.Subject.CommonName)
|
||||||
|
}
|
||||||
|
certDomains = append(certDomains, cert.DNSNames...)
|
||||||
|
|
||||||
|
hasBase := false
|
||||||
|
hasWildcard := false
|
||||||
|
wildcardDomain := "*." + domain
|
||||||
|
|
||||||
|
for _, d := range certDomains {
|
||||||
|
if d == domain {
|
||||||
|
hasBase = true
|
||||||
|
}
|
||||||
|
if d == wildcardDomain {
|
||||||
|
hasWildcard = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasBase {
|
||||||
|
log.Printf("Certificate does not cover base domain: %s", domain)
|
||||||
|
}
|
||||||
|
if !hasWildcard {
|
||||||
|
log.Printf("Certificate does not cover wildcard domain: %s", wildcardDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasBase && hasWildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) loadUserCerts() error {
|
||||||
|
cert, err := tls.LoadX509KeyPair(tm.certPath, tm.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.userCertMu.Lock()
|
||||||
|
tm.userCert = &cert
|
||||||
|
tm.userCertMu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("Loaded user certificates successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) startCertWatcher() {
|
||||||
|
go func() {
|
||||||
|
var lastCertMod, lastKeyMod time.Time
|
||||||
|
|
||||||
|
if info, err := os.Stat(tm.certPath); err == nil {
|
||||||
|
lastCertMod = info.ModTime()
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(tm.keyPath); err == nil {
|
||||||
|
lastKeyMod = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
certInfo, certErr := os.Stat(tm.certPath)
|
||||||
|
keyInfo, keyErr := os.Stat(tm.keyPath)
|
||||||
|
|
||||||
|
if certErr != nil || keyErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if certInfo.ModTime().After(lastCertMod) || keyInfo.ModTime().After(lastKeyMod) {
|
||||||
|
log.Printf("Certificate files changed, reloading...")
|
||||||
|
|
||||||
|
if !ValidateCertDomains(tm.certPath, tm.domain) {
|
||||||
|
log.Printf("New certificates don't cover required domains")
|
||||||
|
|
||||||
|
if !isACMEConfigComplete() {
|
||||||
|
log.Printf("Cannot switch to CertMagic: ACME configuration is incomplete (CF_API_TOKEN is required)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Switching to CertMagic for automatic certificate management")
|
||||||
|
if err := tm.initCertMagic(); err != nil {
|
||||||
|
log.Printf("Failed to initialize CertMagic: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tm.useCertMagic = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tm.loadUserCerts(); err != nil {
|
||||||
|
log.Printf("Failed to reload certificates: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCertMod = certInfo.ModTime()
|
||||||
|
lastKeyMod = keyInfo.ModTime()
|
||||||
|
log.Printf("Certificates reloaded successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) initCertMagic() error {
|
||||||
|
if err := os.MkdirAll(tm.storagePath, 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create cert storage directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acmeEmail := utils.Getenv("ACME_EMAIL", "admin@"+tm.domain)
|
||||||
|
cfAPIToken := utils.Getenv("CF_API_TOKEN", "")
|
||||||
|
acmeStaging := utils.Getenv("ACME_STAGING", "false") == "true"
|
||||||
|
|
||||||
|
if cfAPIToken == "" {
|
||||||
|
return fmt.Errorf("CF_API_TOKEN environment variable is required for automatic certificate generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfProvider := &cloudflare.Provider{
|
||||||
|
APIToken: cfAPIToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := &certmagic.FileStorage{Path: tm.storagePath}
|
||||||
|
|
||||||
|
cache := certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
|
return tm.magic, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
magic := certmagic.New(cache, certmagic.Config{
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
|
||||||
|
acmeIssuer := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
|
||||||
|
Email: acmeEmail,
|
||||||
|
Agreed: true,
|
||||||
|
DNS01Solver: &certmagic.DNS01Solver{
|
||||||
|
DNSManager: certmagic.DNSManager{
|
||||||
|
DNSProvider: cfProvider,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if acmeStaging {
|
||||||
|
acmeIssuer.CA = certmagic.LetsEncryptStagingCA
|
||||||
|
log.Printf("Using Let's Encrypt staging server")
|
||||||
|
} else {
|
||||||
|
acmeIssuer.CA = certmagic.LetsEncryptProductionCA
|
||||||
|
log.Printf("Using Let's Encrypt production server")
|
||||||
|
}
|
||||||
|
|
||||||
|
magic.Issuers = []certmagic.Issuer{acmeIssuer}
|
||||||
|
tm.magic = magic
|
||||||
|
|
||||||
|
domains := []string{tm.domain, "*." + tm.domain}
|
||||||
|
log.Printf("Requesting certificates for: %v", domains)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := magic.ManageSync(ctx, domains); err != nil {
|
||||||
|
return fmt.Errorf("failed to obtain certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Certificates obtained successfully for %v", domains)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) getTLSConfig() *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
GetCertificate: tm.getCertificate,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TLSManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if tm.useCertMagic {
|
||||||
|
return tm.magic.GetCertificate(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.userCertMu.RLock()
|
||||||
|
defer tm.userCertMu.RUnlock()
|
||||||
|
|
||||||
|
if tm.userCert == nil {
|
||||||
|
return nil, fmt.Errorf("no certificate available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm.userCert, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user