feat: replace autocert with lego CLI for reliable LE cert issuance
This commit is contained in:
parent
02bab22a5d
commit
5f168d21f1
5 changed files with 154 additions and 144 deletions
|
|
@ -55,6 +55,12 @@ if [ -d "$TOOL_SRC" ]; then
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install lego if not present (for LE certificate management)
|
||||||
|
if ! command -v lego &>/dev/null && [ -f "$TOOL_BIN" ]; then
|
||||||
|
echo "[*] Installing lego ACME client..."
|
||||||
|
"$TOOL_BIN" install-lego 2>&1 || echo "[WARN] Lego install failed"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Greenfield path ---
|
# --- Greenfield path ---
|
||||||
if [ "$GREENFIELD" = true ]; then
|
if [ "$GREENFIELD" = true ]; then
|
||||||
|
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -4,12 +4,10 @@ go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
go.etcd.io/bbolt v1.5.0
|
go.etcd.io/bbolt v1.5.0
|
||||||
golang.org/x/crypto v0.53.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/net v0.55.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.38.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -6,16 +6,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
|
||||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
|
@ -11,20 +10,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
log.Fatalf("Usage: %s <cert|db> [flags]", os.Args[0])
|
log.Fatalf("Usage: %s <cert|db|install-lego> [flags]", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
|
@ -32,11 +29,67 @@ func main() {
|
||||||
runCert(os.Args[2:])
|
runCert(os.Args[2:])
|
||||||
case "db":
|
case "db":
|
||||||
runDB(os.Args[2:])
|
runDB(os.Args[2:])
|
||||||
|
case "install-lego":
|
||||||
|
runInstallLego()
|
||||||
default:
|
default:
|
||||||
log.Fatalf("Unknown command: %s (use cert or db)", os.Args[1])
|
log.Fatalf("Unknown command: %s (use cert, db, or install-lego)", os.Args[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Lego install ---
|
||||||
|
|
||||||
|
func runInstallLego() {
|
||||||
|
arch, err := exec.Command("uname", "-m").Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to detect architecture: %v", err)
|
||||||
|
}
|
||||||
|
archStr := strings.TrimSpace(string(arch))
|
||||||
|
archStr = strings.ReplaceAll(archStr, "x86_64", "amd64")
|
||||||
|
archStr = strings.ReplaceAll(archStr, "aarch64", "arm64")
|
||||||
|
|
||||||
|
// Get latest version
|
||||||
|
out, err := exec.Command("curl", "-fsSL",
|
||||||
|
"https://api.github.com/repos/go-acme/lego/releases/latest").Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to fetch latest lego version: %v", err)
|
||||||
|
}
|
||||||
|
version := extractLegoVersion(string(out))
|
||||||
|
if version == "" {
|
||||||
|
log.Fatalf("Could not parse latest lego version")
|
||||||
|
}
|
||||||
|
|
||||||
|
tarball := fmt.Sprintf("lego_%s_linux_%s.tar.gz", version, archStr)
|
||||||
|
url := fmt.Sprintf("https://github.com/go-acme/lego/releases/download/%s/%s", version, tarball)
|
||||||
|
|
||||||
|
log.Printf("Downloading lego %s for %s...", version, archStr)
|
||||||
|
if err := exec.Command("curl", "-fLO", url).Run(); err != nil {
|
||||||
|
log.Fatalf("Failed to download lego: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Installing to /usr/local/bin/...")
|
||||||
|
if err := exec.Command("sudo", "tar", "-xzf", tarball, "-C", "/usr/local/bin", "lego").Run(); err != nil {
|
||||||
|
log.Fatalf("Failed to extract lego: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(tarball)
|
||||||
|
log.Printf("[OK] lego %s installed at /usr/local/bin/lego", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractLegoVersion(body string) string {
|
||||||
|
// Simple parser for the GitHub API response
|
||||||
|
marker := `"tag_name": "`
|
||||||
|
idx := strings.Index(body, marker)
|
||||||
|
if idx < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest := body[idx+len(marker):]
|
||||||
|
end := strings.Index(rest, `"`)
|
||||||
|
if end < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return rest[:end]
|
||||||
|
}
|
||||||
|
|
||||||
// --- Cert command ---
|
// --- Cert command ---
|
||||||
|
|
||||||
func runCert(args []string) {
|
func runCert(args []string) {
|
||||||
|
|
@ -44,7 +97,7 @@ func runCert(args []string) {
|
||||||
domainsStr := fs.String("domains", "", "Comma-separated domain list")
|
domainsStr := fs.String("domains", "", "Comma-separated domain list")
|
||||||
email := fs.String("email", "", "ACME email")
|
email := fs.String("email", "", "ACME email")
|
||||||
backupDir := fs.String("backup-dir", "/opt/backup/certs", "Backup directory for certs")
|
backupDir := fs.String("backup-dir", "/opt/backup/certs", "Backup directory for certs")
|
||||||
deployDir := fs.String("deploy-dir", "", "Optional deploy directory to copy certs to")
|
legoDir := fs.String("lego-dir", "/opt/backup/lego", "Lego working directory")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
|
|
||||||
if *domainsStr == "" || *email == "" {
|
if *domainsStr == "" || *email == "" {
|
||||||
|
|
@ -69,167 +122,85 @@ func runCert(args []string) {
|
||||||
needIssue = true
|
needIssue = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Check if cert is from LE (not self-signed fallback)
|
|
||||||
if !isCertFromLE(certFile) {
|
if !isCertFromLE(certFile) {
|
||||||
log.Printf("[INFO] Cert for %s is not from Let's Encrypt (self-signed fallback), reissuing", domain)
|
log.Printf("[INFO] %s: cert is self-signed, reissuing", domain)
|
||||||
needIssue = true
|
needIssue = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Check expiry (renew if within 30 days)
|
|
||||||
if isCertExpired(certFile, 30*24*time.Hour) {
|
if isCertExpired(certFile, 30*24*time.Hour) {
|
||||||
log.Printf("[INFO] Cert for %s expires soon, reissuing", domain)
|
log.Printf("[INFO] %s: cert expires soon, reissuing", domain)
|
||||||
needIssue = true
|
needIssue = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if needIssue {
|
if needIssue {
|
||||||
log.Printf("[INFO] Requesting LE certificates for %v...", domains)
|
// Try lego first
|
||||||
if err := obtainCerts(domains, *email, *backupDir); err != nil {
|
legoPath, legoErr := exec.LookPath("lego")
|
||||||
log.Printf("[WARN] LE cert issuance failed: %v", err)
|
if legoErr == nil {
|
||||||
log.Printf("[INFO] Generating self-signed fallback certs")
|
log.Printf("[INFO] Requesting LE certificates via lego...")
|
||||||
if err := generateSelfSigned(domains, *backupDir); err != nil {
|
if err := obtainCertsLego(domains, *email, *backupDir, *legoDir, legoPath); err != nil {
|
||||||
log.Printf("[WARN] Self-signed fallback also failed: %v", err)
|
log.Printf("[WARN] lego failed: %v", err)
|
||||||
|
log.Printf("[INFO] Generating self-signed fallback certs")
|
||||||
|
generateSelfSigned(domains, *backupDir)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[WARN] lego not found (install with 'nextwks-tool install-lego')")
|
||||||
|
log.Printf("[INFO] Generating self-signed fallback certs")
|
||||||
|
generateSelfSigned(domains, *backupDir)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[OK] All certificates found in backup (dry-run)")
|
log.Printf("[OK] All certificates found in backup (dry-run)")
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
certFile := filepath.Join(*backupDir, domain, "fullchain.pem")
|
certFile := filepath.Join(*backupDir, domain, "fullchain.pem")
|
||||||
expiry := getCertExpiry(certFile)
|
expiry := getCertExpiry(certFile)
|
||||||
log.Printf(" %s — expires %s (dry-run, deploy.sh copies from backup)", domain, expiry.Format(time.RFC3339))
|
log.Printf(" %s — expires %s", domain, expiry.Format(time.RFC3339))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy to deploy-dir if specified (legacy, not used by deploy.sh)
|
|
||||||
if *deployDir != "" {
|
|
||||||
if err := os.MkdirAll(*deployDir, 0755); err != nil {
|
|
||||||
log.Printf("[WARN] Failed to create deploy dir: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, domain := range domains {
|
|
||||||
srcCert := filepath.Join(*backupDir, domain, "fullchain.pem")
|
|
||||||
srcKey := filepath.Join(*backupDir, domain, "privkey.pem")
|
|
||||||
dstCert := filepath.Join(*deployDir, domain+".crt")
|
|
||||||
dstKey := filepath.Join(*deployDir, domain+".key")
|
|
||||||
|
|
||||||
if fileExists(srcCert) && fileExists(srcKey) {
|
|
||||||
copyFile(srcCert, dstCert)
|
|
||||||
copyFile(srcKey, dstKey)
|
|
||||||
log.Printf("[OK] Deployed cert for %s", domain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func obtainCerts(domains []string, email, backupDir string) error {
|
func obtainCertsLego(domains []string, email, backupDir, legoDir, legoPath string) error {
|
||||||
m := &autocert.Manager{
|
if err := os.MkdirAll(legoDir, 0755); err != nil {
|
||||||
Prompt: autocert.AcceptTOS,
|
return fmt.Errorf("creating lego dir: %w", err)
|
||||||
Email: email,
|
|
||||||
Cache: autocert.DirCache(backupDir),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to obtain certs by starting a temporary HTTP server for challenge
|
// Build lego args
|
||||||
ln, err := net.Listen("tcp", ":80")
|
args := []string{
|
||||||
if err != nil {
|
"--email", email,
|
||||||
// Port 80 busy — try to use the autocert client directly without HTTP server
|
"--http",
|
||||||
log.Printf("[WARN] Port 80 not available (%v), trying direct ACME...", err)
|
"--http.port", ":80",
|
||||||
return obtainCertsDirect(domains, email, backupDir)
|
"--path", legoDir,
|
||||||
|
"--accept-tos",
|
||||||
}
|
}
|
||||||
defer ln.Close()
|
for _, d := range domains {
|
||||||
|
args = append(args, "--domains", d)
|
||||||
// Serve HTTP-01 challenge handler
|
|
||||||
srv := &http.Server{
|
|
||||||
Handler: m.HTTPHandler(nil),
|
|
||||||
Addr: ":80",
|
|
||||||
}
|
}
|
||||||
go srv.Serve(ln)
|
args = append(args, "run")
|
||||||
|
|
||||||
// Give LE a moment to validate
|
cmd := exec.Command(legoPath, args...)
|
||||||
time.Sleep(2 * time.Second)
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
log.Printf(" Running: %s %s", legoPath, strings.Join(args, " "))
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("lego run failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy certificates from lego output to backup
|
||||||
|
certDir := filepath.Join(legoDir, "certificates")
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
hello := &tls.ClientHelloInfo{
|
crtSrc := filepath.Join(certDir, domain+".crt")
|
||||||
ServerName: domain,
|
keySrc := filepath.Join(certDir, domain+".key")
|
||||||
}
|
|
||||||
cert, err := m.GetCertificate(hello)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[WARN] Failed to get cert for %s: %v", domain, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Store the cert to backup
|
|
||||||
domainDir := filepath.Join(backupDir, domain)
|
domainDir := filepath.Join(backupDir, domain)
|
||||||
os.MkdirAll(domainDir, 0755)
|
os.MkdirAll(domainDir, 0755)
|
||||||
|
|
||||||
for _, c := range cert.Certificate {
|
if fileExists(crtSrc) && fileExists(keySrc) {
|
||||||
block := &pem.Block{Type: "CERTIFICATE", Bytes: c}
|
copyFile(crtSrc, filepath.Join(domainDir, "fullchain.pem"))
|
||||||
f, err := os.OpenFile(filepath.Join(domainDir, "fullchain.pem"),
|
copyFile(keySrc, filepath.Join(domainDir, "privkey.pem"))
|
||||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
log.Printf("[OK] Certificate obtained for %s", domain)
|
||||||
if err != nil {
|
} else {
|
||||||
return fmt.Errorf("writing cert: %w", err)
|
log.Printf("[WARN] Certificate files not found for %s in %s", domain, certDir)
|
||||||
}
|
|
||||||
pem.Encode(f, block)
|
|
||||||
f.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and save private key
|
|
||||||
if key, ok := cert.PrivateKey.(*rsa.PrivateKey); ok {
|
|
||||||
keyBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
|
||||||
os.WriteFile(filepath.Join(domainDir, "privkey.pem"),
|
|
||||||
pem.EncodeToMemory(keyBlock), 0600)
|
|
||||||
}
|
|
||||||
log.Printf("[OK] Certificate obtained for %s", domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func obtainCertsDirect(domains []string, email, backupDir string) error {
|
|
||||||
// Direct ACME without port 80 — will likely fail but try anyway
|
|
||||||
// This is a simplified fallback
|
|
||||||
return fmt.Errorf("port 80 required for HTTP-01 challenge")
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSelfSigned(domains []string, backupDir string) error {
|
|
||||||
for _, domain := range domains {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
||||||
tmpl := &x509.Certificate{
|
|
||||||
SerialNumber: serial,
|
|
||||||
Subject: pkix.Name{CommonName: domain},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
}
|
|
||||||
if len(domains) > 1 {
|
|
||||||
tmpl.DNSNames = domains
|
|
||||||
}
|
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
domainDir := filepath.Join(backupDir, domain)
|
|
||||||
os.MkdirAll(domainDir, 0755)
|
|
||||||
|
|
||||||
certFile := filepath.Join(domainDir, "fullchain.pem")
|
|
||||||
keyFile := filepath.Join(domainDir, "privkey.pem")
|
|
||||||
|
|
||||||
f, _ := os.Create(certFile)
|
|
||||||
pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
f, _ = os.Create(keyFile)
|
|
||||||
pem.Encode(f, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
log.Printf("[INFO] Self-signed cert generated for %s", domain)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -338,3 +309,44 @@ func copyFile(src, dst string) error {
|
||||||
}
|
}
|
||||||
return os.WriteFile(dst, data, 0644)
|
return os.WriteFile(dst, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateSelfSigned(domains []string, backupDir string) error {
|
||||||
|
for _, domain := range domains {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{CommonName: domain},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
}
|
||||||
|
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
domainDir := filepath.Join(backupDir, domain)
|
||||||
|
os.MkdirAll(domainDir, 0755)
|
||||||
|
|
||||||
|
certFile := filepath.Join(domainDir, "fullchain.pem")
|
||||||
|
keyFile := filepath.Join(domainDir, "privkey.pem")
|
||||||
|
|
||||||
|
f, _ := os.Create(certFile)
|
||||||
|
pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
f, _ = os.Create(keyFile)
|
||||||
|
pem.Encode(f, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
log.Printf("[INFO] Self-signed cert generated for %s", domain)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
tools/nextwks-tool/nextwks-tool
Executable file
BIN
tools/nextwks-tool/nextwks-tool
Executable file
Binary file not shown.
Loading…
Reference in a new issue