fix: ACME ownership self-heal + daily timer, apply-all force, firewall baseline re-stamp
acme:
- acme.sh chmods its tree to owner-only (700/600) every run, which
broke the two-user model: a tree left owner-only by one user made
every acme.sh call of the other exit 2
- normalize_acme_home() reopens group access (sudo chmod g+rwX,
files only — setgid dirs trip RestrictSUIDSGID); _run_acme_preflight
is the choke point before every daemon acme.sh call + startup
- acme service now runs as the daemon user; --log persists the raw CA
transcript; SYS_LOG=6 journals manual issue/renew runs
- timer daily-only: two runs/day landed inside ZeroSSL's 24h
validation backoff (Retry-After: 86400) — a permanent renewal lockout
- _collect_acme no longer raises on cert-list failure; reports
status.error (AcmeState.status) so the certs page can surface it
firewall: re-stamp the applied baseline on live zone mutations
(interfaces/services/rich-rules/masquerade/forward-ports) so cancel-all
reverts to post-mutation state, not a stale install-era snapshot;
set_masquerade syncs the declarative config for existing zones;
add_forward_port records toaddr only with toport
status: apply-all accepts {"force": true} (forwarded to the firewall
apply only); ApplyConfirm force checkbox; applyResultToasts() — the
errors map wins over the 200; ActionButton checks errors before the
success toast; dashboard uses ApplyConfirm
system_import: drift re-imports carry the existing apply-meta; first
import stamps the adopted content as applied (it is the running state)
— no phantom pending changes
nginx: get_config only re-saves when migration actually changed the
config (no more owner/mtime churn on every read)
install: repair mis-owned top-level system dirs (tmpfiles
unsafe-path-transition), warn with a full-repair command for deeper
mis-ownership
daemon/server: loop.get_exception_handler() (aiohttp API fix)
tests: 888 pytest + 24 node passing; ruff clean
This commit is contained in:
@@ -336,6 +336,7 @@ export default definePage({
|
||||
if (guard) return guard;
|
||||
|
||||
const account = state.acme.data?.account || { registered: false, email: '', ca: '' };
|
||||
const certError = state.acme.data?.status?.error;
|
||||
const rows = (state.acme.data?.certs || []).map(c => {
|
||||
const badge = certStatusBadge({ expired: c.expired, daysRemaining: c.days_remaining });
|
||||
|
||||
@@ -363,6 +364,15 @@ export default definePage({
|
||||
onClick=${() => issueCertModal(state)}>Issue Certificate</button>`,
|
||||
}),
|
||||
_accountCard(account),
|
||||
certError
|
||||
? html`<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="text-warning text-sm">
|
||||
Certificate data unavailable: ${esc(certError)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: null,
|
||||
rows.length
|
||||
? Table({
|
||||
columns: ['Domain', 'Issuer', 'Expiry', 'Status', 'Actions'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton, CancelConfirm, fmtBytes } from '/static/hoover/index.js';
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ApplyConfirm, CancelConfirm, fmtBytes } from '/static/hoover/index.js';
|
||||
|
||||
// Render a single firewall change as "current → new".
|
||||
// `live` is the currently applied value; `config` is the target value it
|
||||
@@ -158,7 +158,7 @@ export default definePage({
|
||||
</li>`)}
|
||||
</ul>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<${ActionButton} url="/api/status/apply-all" label="Apply All Changes"
|
||||
<${ApplyConfirm} pending=${true} label="Apply All Changes"
|
||||
successMsg="All changes applied"
|
||||
cls="btn btn-sm btn-primary" />
|
||||
<${CancelConfirm} cls="btn btn-sm btn-danger" />
|
||||
|
||||
Reference in New Issue
Block a user