Skip to content

Commit 974524d

Browse files
committed
fix(nginx): preserve SSL config across domain updates and restarts
writeAndReload() was regenerating nginx configs from a template that only supported HTTP (port 80), overwriting certbot's SSL directives. Now the template conditionally generates full SSL blocks when SSLEnabled=true. SyncConfigs() verifies certificate presence on disk at startup and syncs the SSLEnabled flag accordingly. Added UI button to renew certificates for domains that already have SSL.
1 parent c5af50e commit 974524d

3 files changed

Lines changed: 85 additions & 10 deletions

File tree

internal/nginx/service.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ func (s *Service) SyncConfigs() error {
4040
for i := range domains {
4141
d := &domains[i]
4242

43+
// Sync SSL flag with actual certificate presence on disk
44+
if runtime.GOOS == "linux" {
45+
certExists := s.sslCertExists(d.Domain)
46+
if certExists && !d.SSLEnabled {
47+
slog.Info("SSL certificate found on disk, enabling SSL flag", "domain", d.Domain)
48+
d.SSLEnabled = true
49+
if err := s.repo.Update(d); err != nil {
50+
slog.Error("failed to update SSL flag", "error", err, "domain", d.Domain)
51+
}
52+
} else if !certExists && d.SSLEnabled {
53+
slog.Warn("SSL certificate missing on disk, disabling SSL flag", "domain", d.Domain)
54+
d.SSLEnabled = false
55+
if err := s.repo.Update(d); err != nil {
56+
slog.Error("failed to update SSL flag", "error", err, "domain", d.Domain)
57+
}
58+
}
59+
}
60+
4361
// Migrate: generate missing cookie secret for auth-enabled domains
4462
if d.AuthEnabled && d.AuthCookieSecret == "" {
4563
secret, err := generateCookieSecret()
@@ -338,7 +356,28 @@ func (s *Service) RequestSSL(id int64, email string) error {
338356
}
339357

340358
domain.SSLEnabled = true
341-
return s.repo.Update(domain)
359+
if err := s.repo.Update(domain); err != nil {
360+
return fmt.Errorf("update domain: %w", err)
361+
}
362+
363+
// Regenerate config with SSL directives from our template
364+
if err := s.writeAndReload(domain); err != nil {
365+
slog.Error("failed to regenerate nginx config after SSL", "error", err, "domain", domain.Domain)
366+
}
367+
368+
return nil
369+
}
370+
371+
func (s *Service) sslCertExists(domain string) bool {
372+
certPath := fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", domain)
373+
keyPath := fmt.Sprintf("/etc/letsencrypt/live/%s/privkey.pem", domain)
374+
if _, err := os.Stat(certPath); err != nil {
375+
return false
376+
}
377+
if _, err := os.Stat(keyPath); err != nil {
378+
return false
379+
}
380+
return true
342381
}
343382

344383
func (s *Service) configPath(domain string) string {

internal/nginx/template.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import (
55
"text/template"
66
)
77

8-
const nginxConfTemplate = `# Managed by system-control. Do not edit manually.
9-
server {
10-
listen 80;
11-
server_name {{.Domain}};
12-
8+
// locationBlock is a reusable sub-template for proxy location directives.
9+
const locationBlock = `
1310
{{- if .HasAuth}}
1411
# Cookie-based auth via system-control
1512
location = /_sc_auth_verify {
@@ -58,11 +55,39 @@ server {
5855
location @login_redirect {
5956
return 302 $scheme://$host/_sc_auth/login?domain=$host&redirect=$scheme://$host$request_uri;
6057
}
61-
{{- end}}
58+
{{- end}}`
59+
60+
const nginxConfTemplate = `# Managed by system-control. Do not edit manually.
61+
{{- if .SSLEnabled}}
62+
server {
63+
listen 80;
64+
server_name {{.Domain}};
65+
return 301 https://$host$request_uri;
6266
}
67+
68+
server {
69+
listen 443 ssl;
70+
server_name {{.Domain}};
71+
72+
ssl_certificate /etc/letsencrypt/live/{{.Domain}}/fullchain.pem;
73+
ssl_certificate_key /etc/letsencrypt/live/{{.Domain}}/privkey.pem;
74+
include /etc/letsencrypt/options-ssl-nginx.conf;
75+
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
76+
{{template "locations" .}}
77+
}
78+
{{- else}}
79+
server {
80+
listen 80;
81+
server_name {{.Domain}};
82+
{{template "locations" .}}
83+
}
84+
{{- end}}
6385
`
6486

65-
var confTmpl = template.Must(template.New("nginx").Parse(nginxConfTemplate))
87+
var confTmpl = template.Must(
88+
template.Must(template.New("locations").Parse(locationBlock)).
89+
New("nginx").Parse(nginxConfTemplate),
90+
)
6691

6792
type configData struct {
6893
Domain string
@@ -72,6 +97,7 @@ type configData struct {
7297
UpstreamSSLVerify bool
7398
HasAuth bool
7499
BackendPort int
100+
SSLEnabled bool
75101
}
76102

77103
func renderConfig(d *Domain, backendPort int) ([]byte, error) {
@@ -87,6 +113,7 @@ func renderConfig(d *Domain, backendPort int) ([]byte, error) {
87113
UpstreamSSLVerify: d.UpstreamSSLVerify,
88114
HasAuth: d.AuthEnabled,
89115
BackendPort: backendPort,
116+
SSLEnabled: d.SSLEnabled,
90117
}
91118
var buf bytes.Buffer
92119
if err := confTmpl.Execute(&buf, data); err != nil {

web/src/pages/NginxDomainsPage.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState } from 'react'
22
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3-
import { Plus, Trash2, Shield, ShieldOff, Lock, Download, ExternalLink, Pencil, Palette } from 'lucide-react'
3+
import { Plus, Trash2, Shield, ShieldOff, Lock, Download, ExternalLink, Pencil, Palette, RefreshCw } from 'lucide-react'
44
import { api } from '../api/client'
55
import type { NginxDomain, ExternalNginxDomain, NetworkNode } from '../types'
66
import { Button } from '../components/ui/Button'
@@ -215,7 +215,16 @@ export function NginxDomainsPage() {
215215
<div>
216216
<span className="text-slate-500">SSL</span>
217217
{d.sslEnabled ? (
218-
<p className="text-green-400 flex items-center gap-1"><Shield size={14} /> Active</p>
218+
<div className="flex items-center gap-2">
219+
<p className="text-green-400 flex items-center gap-1"><Shield size={14} /> Active</p>
220+
<button
221+
onClick={() => setSslDomainId(d.id)}
222+
className="text-slate-500 hover:text-yellow-400 transition-colors"
223+
title="Renew certificate"
224+
>
225+
<RefreshCw size={14} />
226+
</button>
227+
</div>
219228
) : (
220229
<button
221230
onClick={() => setSslDomainId(d.id)}

0 commit comments

Comments
 (0)