fix(certificates): не мониторить SSL без health-check с TLS verify
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m59s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s

Биндинги с выключенным health-check (imsk/mmsk) исключаются из авто-мониторинга; sticky footer в редактировании сервиса.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-20 18:09:51 +07:00
co-authored by Cursor
parent 56f2ed78e5
commit 278a6b64b4
4 changed files with 119 additions and 15 deletions
+32 -6
View File
@@ -142,11 +142,27 @@ function bindingSubdomain(
return repos.findSubdomainByDomainAndName(db, domainId, hostname);
}
/** Health-check without TLS verify implies invalid certs — skip SSL monitoring. */
function skipsSslDueToHealthTls(
group: { health_check_enabled: boolean; health_check_verify_tls: boolean } | null,
type HealthTlsFlags = {
health_check_enabled: boolean;
health_check_verify_tls: boolean;
};
/**
* Auto SSL monitoring follows effective health-check with TLS verify.
* No health → no SSL. Health without verify_tls (self-signed) → no SSL.
* Binding inherits group health when its own health is off.
*/
function hasSslHealthGate(
own: HealthTlsFlags,
group: (HealthTlsFlags & { enabled: boolean }) | null,
): boolean {
return Boolean(group?.health_check_enabled && !group.health_check_verify_tls);
if (own.health_check_enabled) {
return own.health_check_verify_tls;
}
if (group?.enabled && group.health_check_enabled) {
return group.health_check_verify_tls;
}
return false;
}
export function buildServiceCertificateFqdns(
@@ -160,7 +176,17 @@ export function buildServiceCertificateFqdns(
? repos.getServiceGroup(db, service.service_group_id)
: null;
if (!shouldMonitorService(service, group)) continue;
if (skipsSslDueToHealthTls(group)) continue;
if (
!hasSslHealthGate(
{
health_check_enabled: binding.health_check_enabled,
health_check_verify_tls: binding.health_check_verify_tls,
},
group,
)
) {
continue;
}
const subdomain = bindingSubdomain(db, binding.domain_id, binding.hostname);
if (subdomain && !subdomain.enabled) continue;
@@ -176,7 +202,7 @@ export function buildServiceCertificateFqdns(
const knownZones = repos.listAllDomains(db).map((d) => d.zone_name);
for (const group of repos.listServiceGroups(db)) {
if (!group.enabled || !group.domain?.trim()) continue;
if (skipsSslDueToHealthTls(group)) continue;
if (!group.health_check_enabled || !group.health_check_verify_tls) continue;
const parsed = parseFqdn(group.domain, knownZones);
if (!parsed) continue;
+81 -1
View File
@@ -73,7 +73,17 @@ describe("certificates", () => {
);
const service = repos.createService(testApp.db, "Web", "web");
repos.setServiceEnabled(testApp.db, service.id, true);
repos.insertBinding(testApp.db, domain.id, service.id, "api", null);
const binding = repos.insertBinding(
testApp.db,
domain.id,
service.id,
"api",
null,
);
repos.updateBindingLbConfig(testApp.db, binding.id, {
health_check_enabled: true,
health_check_verify_tls: true,
});
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
@@ -94,6 +104,76 @@ describe("certificates", () => {
await testApp.close();
});
it("does not monitor binding when health-check is off", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"rkns.example.com",
"cf-zone-imsk",
);
const service = repos.createService(testApp.db, "Cname", "cname");
repos.setServiceEnabled(testApp.db, service.id, true);
const binding = repos.insertBinding(
testApp.db,
domain.id,
service.id,
"imsk",
null,
);
repos.setBindingCnameTarget(testApp.db, binding.id, "ihome.rkns.example.com");
repos.updateBindingLbConfig(testApp.db, binding.id, {
health_check_enabled: false,
});
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
"imsk.rkns.example.com",
null,
CERT_ERROR,
"stale",
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some(
(c) => c.hostname === "imsk.rkns.example.com",
),
).toBe(false);
expect(certificateService.checkHostname).not.toHaveBeenCalled();
const listRes = await testApp.inject({
method: "GET",
url: "/api/v1/certificates",
headers,
});
expect(listRes.statusCode).toBe(200);
expect(
(listRes.json() as { hostname: string }[]).some(
(c) => c.hostname === "imsk.rkns.example.com",
),
).toBe(false);
await testApp.close();
});
it("does not monitor host when service is disabled", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
@@ -381,8 +381,8 @@ export function ServiceEditSheet({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex w-full flex-col gap-0 overflow-y-auto sm:max-w-xl">
<SheetHeader className="border-b pb-4">
<SheetContent className="flex w-full flex-col gap-0 overflow-hidden sm:max-w-xl">
<SheetHeader className="shrink-0 border-b pb-4">
<SheetTitle>{isCreate ? 'Новый сервис' : 'Редактирование сервиса'}</SheetTitle>
<SheetDescription>
Настройте параметры сервиса и привязки FQDN IP или CNAME. Зона определяется из FQDN
@@ -390,7 +390,7 @@ export function ServiceEditSheet({
</SheetDescription>
</SheetHeader>
<div className="flex flex-1 flex-col gap-4 px-4 py-4">
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">
<CountedLineTabs
tabs={[
{ id: 'general', label: 'Основное' },
@@ -642,9 +642,7 @@ export function ServiceEditSheet({
</CountedLineTabs>
</div>
<Separator />
<SheetFooter className="flex flex-row flex-wrap gap-2 border-t-0 pt-4">
<SheetFooter className="shrink-0 flex flex-row flex-wrap gap-2 border-t pt-4">
{!isCreate ? (
<ConfirmDialog
trigger={
+2 -2
View File
@@ -74,7 +74,7 @@ function CertificatesPage() {
<PageShell>
<PageHeader
title="Сертификаты"
description="Мониторинг SSL: хосты с активными сервисами или режимом «Обязательно»"
description="Мониторинг SSL: health-check с проверкой TLS, либо режим «Обязательно»"
actions={primaryAction}
/>
<CertKpiCards
@@ -86,7 +86,7 @@ function CertificatesPage() {
/>
<ResourcePage
title="Сертификаты"
description="Мониторинг SSL: хосты с активными сервисами или режимом «Обязательно»"
description="Мониторинг SSL: health-check с проверкой TLS, либо режим «Обязательно»"
hideHeader
tabs={CERT_TABS.map((tab) => ({ ...tab }))}
activeTab={activeTab}