Certificates guide: keep a domain's own hostnames #164
No files matched your search
@@ -41,6 +41,8 @@ interface DomainRow {
|
||||
name: string;
|
||||
automatic: boolean;
|
||||
providerId?: string;
|
||||
/** The domain's own certificate hostnames, kept as they are when it's switched. */
|
||||
hostnames: string[];
|
||||
dnsAutomatic: boolean;
|
||||
zone: ReturnType<typeof parseZone>;
|
||||
}
|
||||
@@ -93,6 +95,7 @@ async function load(): Promise<Loaded> {
|
||||
name: String(d.name),
|
||||
automatic: cm['@type'] === 'Automatic',
|
||||
providerId: cm.acmeProviderId as string | undefined,
|
||||
hostnames: cm['@type'] === 'Automatic' ? keys(cm.subjectAlternativeNames) : [],
|
||||
dnsAutomatic: dm['@type'] === 'Automatic',
|
||||
zone: parseZone(d.dnsZoneFile as string | undefined),
|
||||
};
|
||||
@@ -113,6 +116,9 @@ async function load(): Promise<Loaded> {
|
||||
};
|
||||
}
|
||||
|
||||
/** "a and b" for the default pair, "a, b, c" for a domain's own list. */
|
||||
const joinNames = (names: string[]) => (names.length === 2 ? names.join(' and ') : names.join(', '));
|
||||
|
||||
/** When the switch happened, read in the click handler, for telling a new certificate from an old one. */
|
||||
async function switchedAt(): Promise<number> {
|
||||
return Date.now();
|
||||
@@ -206,7 +212,7 @@ export function CertificateSetupPage() {
|
||||
const chosen = useMemo(() => (data ? data.domains.filter((d) => picked.has(d.id)) : []), [data, picked]);
|
||||
const methodOf = (d: DomainRow): Method => (d.dnsAutomatic ? 'dns' : 'tls');
|
||||
const namesOf = useCallback(
|
||||
(d: DomainRow) => orderNames(d.name, methodOf(d), data?.serverName ?? '', d.zone),
|
||||
(d: DomainRow) => orderNames(d.name, methodOf(d), data?.serverName ?? '', d.zone, d.hostnames),
|
||||
[data],
|
||||
);
|
||||
|
||||
@@ -344,7 +350,7 @@ export function CertificateSetupPage() {
|
||||
certificateManagement: {
|
||||
'@type': 'Automatic',
|
||||
acmeProviderId: ids[methodOf(d)],
|
||||
subjectAlternativeNames: {},
|
||||
subjectAlternativeNames: Object.fromEntries(d.hostnames.map((h) => [h, true])),
|
||||
},
|
||||
},
|
||||
]),
|
||||
@@ -506,7 +512,7 @@ export function CertificateSetupPage() {
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
||||
{t('certSetup.dnsReady', '{{domain}}: checked through its connected DNS, covering {{names}}.', {
|
||||
domain: d.name,
|
||||
names: namesOf(d).join(' and '),
|
||||
names: joinNames(namesOf(d)),
|
||||
})}
|
||||
</p>
|
||||
))}
|
||||
|
||||
@@ -27,6 +27,13 @@ describe('orderNames', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses a domain's own hostnames in place of the defaults, with either method", () => {
|
||||
const own = ['mail', 'Example.com', 'smtp.example.com', 'mail'];
|
||||
const want = ['mail.example.com', 'example.com', 'smtp.example.com'];
|
||||
expect(orderNames('example.com', 'dns', 'mail.example.com', [], own)).toEqual(want);
|
||||
expect(orderNames('example.com', 'tls', 'mail.example.com', [], own)).toEqual(want);
|
||||
});
|
||||
|
||||
it('leaves out a server name in another domain', () => {
|
||||
expect(orderNames('example.com', 'tls', 'mail.other.net')).not.toContain('mail.other.net');
|
||||
});
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
* The server decides the names (common/src/network/acme/order.rs): with DNS
|
||||
* validation it asks for the domain and a wildcard under it; otherwise for
|
||||
* mta-sts, ua-auto-config, autoconfig and autodiscover under the domain, plus
|
||||
* the server's own name when it's inside the domain. Every name must pass, or
|
||||
* the whole order fails and keeps retrying, so the guide checks them first.
|
||||
* the server's own name when it's inside the domain. A domain's own hostname
|
||||
* list replaces all of that: a name without a dot gets the domain added, one
|
||||
* with a dot is used as written. Every name must pass, or the whole order
|
||||
* fails and keeps retrying, so the guide checks them first.
|
||||
*/
|
||||
|
||||
import type { ZoneRecord } from '@/features/dns/zone';
|
||||
@@ -25,8 +27,17 @@ function inZone(name: string, domain: string): boolean {
|
||||
return name === domain || name.endsWith(`.${domain}`);
|
||||
}
|
||||
|
||||
export function orderNames(domain: string, method: Method, serverName: string, zone: ZoneRecord[] = []): string[] {
|
||||
export function orderNames(
|
||||
domain: string,
|
||||
method: Method,
|
||||
serverName: string,
|
||||
zone: ZoneRecord[] = [],
|
||||
hostnames: string[] = [],
|
||||
): string[] {
|
||||
const d = domain.toLowerCase();
|
||||
if (hostnames.length > 0) {
|
||||
return [...new Set(hostnames.map((h) => (h.includes('.') ? h : `${h}.${d}`).toLowerCase()))];
|
||||
}
|
||||
if (method === 'dns') return [`*.${d}`, d];
|
||||
const names = new Set(TECHNICAL_NAMES.map((h) => `${h}.${d}`));
|
||||
const host = serverName.toLowerCase().replace(/\.$/, '');
|
||||
|
||||
Reference in new issue
Block a user