diff --git a/.env.example b/.env.example
index 5a60e8dd..98381560 100644
--- a/.env.example
+++ b/.env.example
@@ -101,10 +101,6 @@ OAUTH_ALLOW_AUTO_LINKING=false # Auto-link OAuth to accounts without pas
# Certificate storage directory (usually no need to change)
# CERTS_DIRECTORY=./data/certs
-# Caddy certificate directory for ACME metadata scanning in the Certificates page
-# (Only needed for custom/non-standard deployments)
-# CADDY_CERTS_DIR=/caddy-data/caddy/certificates
-
# Login rate limiting (optional, for custom rate limit settings)
# LOGIN_MAX_ATTEMPTS=5
# LOGIN_WINDOW_MS=300000
diff --git a/README.md b/README.md
index c07cf606..3aab0c2c 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,6 @@ Data persists in Docker volumes (caddy-manager-data, caddy-data, caddy-config, c
| `CADDY_API_URL` | Caddy Admin API endpoint | `http://caddy:2019` (prod)
`http://localhost:2019` (dev) | No |
| `DATABASE_URL` | SQLite database URL | `file:/app/data/caddy-proxy-manager.db` | No |
| `CERTS_DIRECTORY` | Certificate storage directory | `./data/certs` | No |
-| `CADDY_CERTS_DIR` | Caddy cert storage path used for ACME metadata scanning (non-default deployments) | `/caddy-data/caddy/certificates` | No |
| `LOGIN_MAX_ATTEMPTS` | Max login attempts before rate limit | `5` | No |
| `LOGIN_WINDOW_MS` | Rate limit window in milliseconds | `300000` (5 min) | No |
| `LOGIN_BLOCK_MS` | Rate limit block duration in milliseconds | `900000` (15 min) | No |
diff --git a/app/(dashboard)/certificates/CertificatesClient.tsx b/app/(dashboard)/certificates/CertificatesClient.tsx
index a78185e8..1c88950b 100644
--- a/app/(dashboard)/certificates/CertificatesClient.tsx
+++ b/app/(dashboard)/certificates/CertificatesClient.tsx
@@ -44,7 +44,6 @@ export default function CertificatesClient({
const [statusFilter, setStatusFilter] = useState(null);
const allStatuses: (CertExpiryStatus | null)[] = [
- ...acmeHosts.map((h) => h.certExpiryStatus),
...importedCerts.map((c) => c.expiryStatus),
];
const { expired, expiringSoon, healthy } = countExpiry(allStatuses);
diff --git a/app/(dashboard)/certificates/components/AcmeTab.tsx b/app/(dashboard)/certificates/components/AcmeTab.tsx
index 0d86f09e..1b6606ac 100644
--- a/app/(dashboard)/certificates/components/AcmeTab.tsx
+++ b/app/(dashboard)/certificates/components/AcmeTab.tsx
@@ -5,7 +5,6 @@ import { Card, CardContent } from "@/components/ui/card";
import { DataTable } from "@/components/ui/DataTable";
import { StatusChip } from "@/components/ui/StatusChip";
import type { AcmeHost } from "../page";
-import { RelativeTime } from "./RelativeTime";
type Props = {
acmeHosts: AcmeHost[];
@@ -40,18 +39,6 @@ const columns = [
),
},
- {
- id: "issuer",
- label: "Issuer",
- render: (r: AcmeHost) => (
- {r.certIssuer ?? "—"}
- ),
- },
- {
- id: "expiry",
- label: "Expiry",
- render: (r: AcmeHost) => ,
- },
{
id: "status",
label: "Status",
@@ -71,7 +58,6 @@ function acmeMobileCard(r: AcmeHost) {
{r.domains[0]}{r.domains.length > 1 ? ` +${r.domains.length - 1}` : ""}
-
@@ -81,7 +67,7 @@ function acmeMobileCard(r: AcmeHost) {
export function AcmeTab({ acmeHosts, acmePagination, search, statusFilter }: Props) {
const filtered = acmeHosts.filter((h) => {
- if (statusFilter && h.certExpiryStatus !== statusFilter) return false;
+ if (statusFilter) return false; // ACME hosts have no expiry status
if (search) {
const q = search.toLowerCase();
return (
diff --git a/app/(dashboard)/certificates/page.tsx b/app/(dashboard)/certificates/page.tsx
index b7a892a7..1d20c2cf 100644
--- a/app/(dashboard)/certificates/page.tsx
+++ b/app/(dashboard)/certificates/page.tsx
@@ -4,7 +4,6 @@ import { proxyHosts, certificates } from '@/src/lib/db/schema';
import { isNull, isNotNull, count } from 'drizzle-orm';
import { requireAdmin } from '@/src/lib/auth';
import CertificatesClient from './CertificatesClient';
-import { scanAcmeCerts } from '@/src/lib/acme-certs';
import { listCaCertificates, type CaCertificate } from '@/src/lib/models/ca-certificates';
import { listIssuedClientCertificates, type IssuedClientCertificate } from '@/src/lib/models/issued-client-certificates';
@@ -23,10 +22,6 @@ export type AcmeHost = {
domains: string[];
ssl_forced: boolean;
enabled: boolean;
- certValidTo: string | null;
- certValidFrom: string | null;
- certIssuer: string | null;
- certExpiryStatus: CertExpiryStatus | null;
};
export type ImportedCertView = {
@@ -86,8 +81,6 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
const offset = (page - 1) * PER_PAGE;
- const acmeCertMap = scanAcmeCerts();
-
const [caCerts, issuedClientCerts] = await Promise.all([
listCaCertificates(),
listIssuedClientCertificates()
@@ -124,25 +117,13 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
.where(isNotNull(proxyHosts.certificateId)),
]);
- const acmeHosts: AcmeHost[] = acmeRows.map(r => {
- const domains = JSON.parse(r.domains) as string[];
- let certInfo = null;
- for (const domain of domains) {
- const info = acmeCertMap.get(domain.toLowerCase());
- if (info) { certInfo = info; break; }
- }
- return {
- id: r.id,
- name: r.name,
- domains,
- ssl_forced: r.sslForced,
- enabled: r.enabled,
- certValidTo: certInfo?.validTo ?? null,
- certValidFrom: certInfo?.validFrom ?? null,
- certIssuer: certInfo?.issuer ?? null,
- certExpiryStatus: certInfo?.validTo ? getExpiryStatus(certInfo.validTo) : null,
- };
- });
+ const acmeHosts: AcmeHost[] = acmeRows.map(r => ({
+ id: r.id,
+ name: r.name,
+ domains: JSON.parse(r.domains) as string[],
+ ssl_forced: r.sslForced,
+ enabled: r.enabled,
+ }));
const usageMap = new Map();
for (const u of usageRows) {
diff --git a/docker-compose.yml b/docker-compose.yml
index d8bcc4f8..ff0b14b9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -58,7 +58,6 @@ services:
- caddy-manager-data:/app/data
- geoip-data:/usr/share/GeoIP:ro,z
- caddy-logs:/logs:ro
- - caddy-data:/caddy-data:ro
depends_on:
caddy:
condition: service_healthy
diff --git a/src/lib/acme-certs.ts b/src/lib/acme-certs.ts
deleted file mode 100644
index 670566af..00000000
--- a/src/lib/acme-certs.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { readdirSync, readFileSync, statSync } from 'node:fs';
-import { join } from 'node:path';
-import { X509Certificate } from 'node:crypto';
-
-export type AcmeCertInfo = {
- validTo: string;
- validFrom: string;
- issuer: string;
- domains: string[];
-};
-
-/**
- * Walks Caddy's certificate storage directory and parses every .crt file.
- * Returns a map from lowercase domain → cert info (most recent cert wins for
- * a given domain if multiple exist).
- *
- * Caddy stores certs under:
- * /acme-v02.api.letsencrypt.org-directory//.crt
- * /acme.zerossl.com-v2-DV90//.crt
- * ...etc
- *
- * The directory is mounted at /caddy-data in the web container, so:
- * CADDY_CERTS_DIR defaults to /caddy-data/caddy/certificates
- */
-const CADDY_CERTS_DIR =
- process.env.CADDY_CERTS_DIR ?? '/caddy-data/caddy/certificates';
-
-function walkCrtFiles(dir: string): string[] {
- const results: string[] = [];
- let entries: string[];
- try {
- entries = readdirSync(dir);
- } catch {
- return results; // directory doesn't exist yet (e.g. no certs issued)
- }
- for (const entry of entries) {
- const full = join(dir, entry);
- try {
- const stat = statSync(full);
- if (stat.isDirectory()) {
- results.push(...walkCrtFiles(full));
- } else if (entry.endsWith('.crt')) {
- results.push(full);
- }
- } catch {
- // skip unreadable entries
- }
- }
- return results;
-}
-
-export function scanAcmeCerts(): Map {
- const map = new Map();
- const crtFiles = walkCrtFiles(CADDY_CERTS_DIR);
-
- for (const file of crtFiles) {
- try {
- const pem = readFileSync(file, 'utf-8');
- const cert = new X509Certificate(pem);
-
- const sanDomains =
- cert.subjectAltName
- ?.split(',')
- .map(s => s.trim())
- .filter(s => s.startsWith('DNS:'))
- .map(s => s.slice(4).toLowerCase()) ?? [];
-
- const issuerLine = cert.issuer ?? '';
- const issuer = (
- issuerLine.match(/O=([^\n,]+)/)?.[1] ??
- issuerLine.match(/CN=([^\n,]+)/)?.[1] ??
- issuerLine
- ).trim();
-
- const info: AcmeCertInfo = {
- validTo: new Date(cert.validTo).toISOString(),
- validFrom: new Date(cert.validFrom).toISOString(),
- issuer,
- domains: sanDomains,
- };
-
- for (const domain of sanDomains) {
- // Keep the cert with the latest validTo for each domain
- const existing = map.get(domain);
- if (!existing || new Date(info.validTo).getTime() > new Date(existing.validTo).getTime()) {
- map.set(domain, info);
- }
- }
- } catch {
- // skip unreadable / malformed certs
- }
- }
-
- return map;
-}
diff --git a/tests/helpers/proxy-api.ts b/tests/helpers/proxy-api.ts
index 53caf3c6..c8d98999 100644
--- a/tests/helpers/proxy-api.ts
+++ b/tests/helpers/proxy-api.ts
@@ -90,7 +90,7 @@ export async function createProxyHost(page: Page, config: ProxyHostConfig): Prom
await accessListTrigger.scrollIntoViewIfNeeded();
await accessListTrigger.click();
const option = page.getByRole('option', { name: config.accessListName });
- await expect(option).toBeVisible({ timeout: 5_000 });
+ await expect(option).toBeVisible({ timeout: 10_000 });
await option.click();
}