The certificates page has been completely redesigned
This commit is contained in:
36
README.md
36
README.md
@@ -116,7 +116,7 @@ Visit `http://localhost:3000/login` and sign in with your credentials.
|
||||
| **Redirects** | Set up 301/302 redirects with optional query string preservation |
|
||||
| **Dead Hosts** | Display branded maintenance pages with custom status codes |
|
||||
| **Access Lists** | Create HTTP basic-auth user lists and assign them to proxy hosts |
|
||||
| **Certificates** | Request ACME-managed certificates or import custom PEM files |
|
||||
| **Certificates** | Import custom SSL/TLS certificates (internal CA, wildcards, etc.) - Caddy auto-manages public certs |
|
||||
| **Settings** | Configure primary domain, ACME email, and Cloudflare DNS automation |
|
||||
| **Audit Log** | Review chronological feed of all administrative actions |
|
||||
|
||||
@@ -253,6 +253,40 @@ npm run dev
|
||||
|
||||
---
|
||||
|
||||
## Certificate Management
|
||||
|
||||
### Automatic HTTPS (Default)
|
||||
|
||||
Caddy automatically handles SSL/TLS certificates for all proxy hosts:
|
||||
|
||||
- **Zero Configuration**: Just add a domain to a proxy host - certificates are obtained automatically
|
||||
- **Auto-Renewal**: Certificates renew automatically before expiration
|
||||
- **Multiple Domains**: Each proxy host can have multiple domains with automatic cert management
|
||||
- **Wildcard Support**: Use Cloudflare DNS-01 challenge for wildcard certificates
|
||||
|
||||
**No action required** - this works out of the box!
|
||||
|
||||
### Custom Certificates (Optional)
|
||||
|
||||
Import your own certificates when you need to:
|
||||
|
||||
- **Internal CA**: Use certificates from your organization's Certificate Authority
|
||||
- **Pre-existing Certs**: Reuse certificates you already have
|
||||
- **Special Requirements**: Compliance, security policies, or specific certificate features
|
||||
- **Wildcard from DNS Provider**: Import wildcard certificates from your DNS provider
|
||||
|
||||
**How to import:**
|
||||
1. Navigate to **Certificates** page
|
||||
2. Click **Import Custom Certificate**
|
||||
3. Provide certificate name and domains
|
||||
4. Paste certificate PEM (full chain recommended)
|
||||
5. Paste private key PEM
|
||||
6. Save and assign to proxy hosts as needed
|
||||
|
||||
**Security Note**: Imported private keys are stored in the database. Ensure your `.env` file and database have restricted permissions (`chmod 600`).
|
||||
|
||||
---
|
||||
|
||||
## Cloudflare DNS Automation
|
||||
|
||||
To enable automatic SSL certificates with Cloudflare DNS-01 challenges:
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
Checkbox
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import type { Certificate } from "@/src/lib/models/certificates";
|
||||
import { createCertificateAction, deleteCertificateAction, updateCertificateAction } from "./actions";
|
||||
@@ -25,139 +24,263 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function CertificatesClient({ certificates }: Props) {
|
||||
const importedCerts = certificates.filter(c => c.type === "imported");
|
||||
const managedCerts = certificates.filter(c => c.type === "managed");
|
||||
|
||||
return (
|
||||
<Stack spacing={4} sx={{ width: "100%" }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h4" fontWeight={600}>
|
||||
Certificates
|
||||
SSL/TLS Certificates
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
Manage ACME-managed certificates or import your own PEM files for custom deployments.
|
||||
Caddy automatically handles HTTPS certificates for all proxy hosts using Let's Encrypt.
|
||||
Import custom certificates only when needed (internal CA, special requirements, etc.).
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={3}>
|
||||
{certificates.map((cert) => (
|
||||
<Card key={cert.id}>
|
||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{cert.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{cert.domain_names.join(", ")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip label={cert.type === "managed" ? "Managed" : "Imported"} color="primary" variant="outlined" />
|
||||
</Box>
|
||||
<Alert severity="info" icon={<InfoOutlinedIcon />}>
|
||||
<Typography variant="body2" fontWeight={600} gutterBottom>
|
||||
How Caddy handles certificates:
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div">
|
||||
• <strong>Automatic HTTPS:</strong> Caddy automatically obtains and renews certificates for all domains
|
||||
<br />
|
||||
• <strong>No configuration needed:</strong> Just add a proxy host with a domain, and Caddy handles the rest
|
||||
<br />
|
||||
• <strong>Custom certificates:</strong> Import your own certificates only when you have specific requirements
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Accordion elevation={0} disableGutters>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0 }}>
|
||||
<Typography fontWeight={600}>Edit</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: 0 }}>
|
||||
<Stack component="form" action={(formData) => updateCertificateAction(cert.id, formData)} spacing={2}>
|
||||
<TextField name="name" label="Name" defaultValue={cert.name} fullWidth />
|
||||
<TextField
|
||||
name="domain_names"
|
||||
label="Domains"
|
||||
defaultValue={cert.domain_names.join("\n")}
|
||||
multiline
|
||||
minRows={3}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField select name="type" label="Type" defaultValue={cert.type} fullWidth>
|
||||
<MenuItem value="managed">Managed (ACME)</MenuItem>
|
||||
<MenuItem value="imported">Imported</MenuItem>
|
||||
</TextField>
|
||||
{cert.type === "managed" ? (
|
||||
<Box>
|
||||
{managedCerts.length > 0 && (
|
||||
<Stack spacing={2}>
|
||||
<Alert severity="warning">
|
||||
<Typography variant="body2">
|
||||
<strong>Legacy "Managed" certificates detected:</strong> These entries are redundant since Caddy automatically manages HTTPS.
|
||||
Consider deleting them unless you need to explicitly track certificate usage.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Managed Certificates (Legacy)
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{managedCerts.map((cert) => (
|
||||
<Card key={cert.id} sx={{ bgcolor: 'action.hover' }}>
|
||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{cert.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{cert.domain_names.join(", ")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Chip label="Managed by Caddy" color="info" size="small" />
|
||||
<Chip label="Auto-Renew" color="success" size="small" variant="outlined" />
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Accordion elevation={0} disableGutters sx={{ bgcolor: 'transparent' }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0 }}>
|
||||
<Typography fontWeight={600}>Edit / Delete</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: 0 }}>
|
||||
<Stack component="form" action={(formData) => updateCertificateAction(cert.id, formData)} spacing={2}>
|
||||
<TextField name="name" label="Name" defaultValue={cert.name} fullWidth />
|
||||
<TextField
|
||||
name="domain_names"
|
||||
label="Domains (one per line)"
|
||||
defaultValue={cert.domain_names.join("\n")}
|
||||
multiline
|
||||
minRows={3}
|
||||
fullWidth
|
||||
helperText="These domains will be automatically managed by Caddy's ACME"
|
||||
/>
|
||||
<input type="hidden" name="type" value="managed" />
|
||||
<input type="hidden" name="auto_renew" value="on" />
|
||||
<input type="hidden" name="auto_renew_present" value="1" />
|
||||
<FormControlLabel control={<Checkbox name="auto_renew" defaultChecked={cert.auto_renew} />} label="Auto renew" />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<Button type="submit" variant="contained">
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
formAction={deleteCertificateAction.bind(null, cert.id)}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{importedCerts.length > 0 && (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Imported Certificates
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{importedCerts.map((cert) => (
|
||||
<Card key={cert.id}>
|
||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{cert.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{cert.domain_names.join(", ")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip label="Custom Certificate" color="secondary" />
|
||||
</Box>
|
||||
|
||||
<Accordion elevation={0} disableGutters>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ px: 0 }}>
|
||||
<Typography fontWeight={600}>Edit / Delete</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ px: 0 }}>
|
||||
<Stack component="form" action={(formData) => updateCertificateAction(cert.id, formData)} spacing={2}>
|
||||
<TextField name="name" label="Name" defaultValue={cert.name} fullWidth />
|
||||
<TextField
|
||||
name="domain_names"
|
||||
label="Domains (one per line)"
|
||||
defaultValue={cert.domain_names.join("\n")}
|
||||
multiline
|
||||
minRows={3}
|
||||
fullWidth
|
||||
helperText="Domains covered by this certificate"
|
||||
/>
|
||||
<input type="hidden" name="type" value="imported" />
|
||||
|
||||
<TextField
|
||||
name="certificate_pem"
|
||||
label="Certificate PEM"
|
||||
placeholder="-----BEGIN CERTIFICATE-----"
|
||||
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||
multiline
|
||||
minRows={6}
|
||||
fullWidth
|
||||
helperText="Full chain recommended (cert + intermediates)"
|
||||
/>
|
||||
<TextField
|
||||
name="private_key_pem"
|
||||
label="Private key PEM"
|
||||
placeholder="-----BEGIN PRIVATE KEY-----"
|
||||
label="Private Key PEM"
|
||||
placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
|
||||
multiline
|
||||
minRows={6}
|
||||
fullWidth
|
||||
helperText="Keep this secure! Never share your private key"
|
||||
type="password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<Button type="submit" variant="contained">
|
||||
Save certificate
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
formAction={deleteCertificateAction.bind(null, cert.id)}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<Button type="submit" variant="contained">
|
||||
Update Certificate
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
formAction={deleteCertificateAction.bind(null, cert.id)}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack spacing={2} component="section">
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Create certificate
|
||||
Import Custom Certificate
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>When to import certificates:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" component="ul" sx={{ mt: 1, mb: 0, pl: 2 }}>
|
||||
<li>Using an internal Certificate Authority (CA)</li>
|
||||
<li>Wildcard certificates from your DNS provider</li>
|
||||
<li>Pre-existing certificates you want to reuse</li>
|
||||
<li>Special compliance or security requirements</li>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
<strong>Otherwise:</strong> Just create a proxy host with your domain - Caddy will handle everything automatically!
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack component="form" action={createCertificateAction} spacing={2}>
|
||||
<TextField name="name" label="Name" placeholder="Wildcard certificate" required fullWidth />
|
||||
<TextField
|
||||
name="name"
|
||||
label="Certificate Name"
|
||||
placeholder="Internal CA Certificate"
|
||||
required
|
||||
fullWidth
|
||||
helperText="Descriptive name to identify this certificate"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name="domain_names"
|
||||
label="Domains"
|
||||
placeholder="example.com"
|
||||
label="Domains (one per line)"
|
||||
placeholder="*.example.com example.com"
|
||||
multiline
|
||||
minRows={3}
|
||||
required
|
||||
fullWidth
|
||||
helperText="List all domains/subdomains covered by this certificate"
|
||||
/>
|
||||
<TextField select name="type" label="Type" defaultValue="managed" fullWidth>
|
||||
<MenuItem value="managed">Managed (ACME)</MenuItem>
|
||||
<MenuItem value="imported">Imported</MenuItem>
|
||||
</TextField>
|
||||
<FormControlLabel control={<Checkbox name="auto_renew" defaultChecked />} label="Auto renew (managed only)" />
|
||||
|
||||
<input type="hidden" name="type" value="imported" />
|
||||
|
||||
<TextField
|
||||
name="certificate_pem"
|
||||
label="Certificate PEM"
|
||||
placeholder="Paste PEM content for imported certificates"
|
||||
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||
multiline
|
||||
minRows={5}
|
||||
minRows={8}
|
||||
required
|
||||
fullWidth
|
||||
helperText="Paste the full certificate chain (certificate + intermediate certificates)"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name="private_key_pem"
|
||||
label="Private key PEM"
|
||||
placeholder="Paste PEM key for imported certificates"
|
||||
label="Private Key PEM"
|
||||
placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
|
||||
multiline
|
||||
minRows={5}
|
||||
minRows={8}
|
||||
required
|
||||
fullWidth
|
||||
helperText="Private key for this certificate. Stored securely."
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button type="submit" variant="contained">
|
||||
Create certificate
|
||||
<Button type="submit" variant="contained" size="large">
|
||||
Import Certificate
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user