Commit 8b60f55e authored by nanahira's avatar nanahira

add auto sign certificate

parent 7ec69b9c
Pipeline #26557 passed with stages
in 19 minutes and 11 seconds
......@@ -15,5 +15,6 @@ async function main() {
{ escape: (v) => v },
),
);
process.exit(0);
}
main();
This diff is collapsed.
......@@ -56,6 +56,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"acme-client": "^5.3.0",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"nano-md5": "^1.0.5"
......
import { createServer, Server } from 'http';
import acme, { Client } from 'acme-client';
import fs from 'fs';
let email: string;
export const domainsToBeSigned: string[] = [];
export function addSignCert(domains: string[], payload: string) {
domainsToBeSigned.push(...domains);
if (!email) {
// acme://
email = payload.slice(7);
}
return domainsToBeSigned[0];
}
export async function runSignCert() {
if (!domainsToBeSigned.length) {
return;
}
console.error(`Signing certificate for ${domainsToBeSigned.join(', ')}`);
// sign a cert with acme-client
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: domainsToBeSigned[0],
altNames: domainsToBeSigned.slice(1),
});
// store as token
const contentMap = new Map<string, string>();
const server = createServer(async (req, res) => {
// read token from http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>
const token = req.url?.split('/.well-known/acme-challenge/').pop();
if (!token) {
// 404
res.writeHead(404);
res.end('Token not Found');
return;
}
const content = contentMap.get(token);
if (!content) {
// 404
res.writeHead(404);
res.end('Content not Found');
return;
}
res.writeHead(200);
res.end(content);
}).listen(80);
await fs.promises.mkdir('/etc/nginx/acme', { recursive: true });
let accountKey: Buffer;
try {
accountKey = await fs.promises.readFile('/etc/nginx/acme/account.pem');
} catch (e) {
accountKey = await acme.forge.createPrivateKey();
await fs.promises.writeFile('/etc/nginx/acme/account.pem', accountKey);
}
const acmeClient = new Client({
directoryUrl: acme.directory.letsencrypt.production,
accountKey,
});
try {
const certificate = await acmeClient.auto({
csr: certificateRequest,
email,
termsOfServiceAgreed: true,
challengePriority: ['http-01'],
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== 'http-01') {
return;
}
// store token
console.error(
`Storing token for ${challenge.token}: ${keyAuthorization}`,
);
contentMap.set(challenge.token, keyAuthorization);
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// does nothing
},
});
console.error('Certificate signed');
// save certificate as fullchain.pem and key as privkey.pem
await fs.promises.mkdir(`/etc/nginx/certs/${domainsToBeSigned[0]}`, {
recursive: true,
});
await fs.promises.writeFile(
`/etc/nginx/certs/${domainsToBeSigned[0]}/fullchain.pem`,
certificate,
);
await fs.promises.writeFile(
`/etc/nginx/certs/${domainsToBeSigned[0]}/privkey.pem`,
certificateKey,
);
} catch (e) {
console.error(
`Failed to sign certificate for ${domainsToBeSigned.join(', ')}: ${
e.stack
}`,
);
}
server.close();
}
......@@ -3,6 +3,7 @@ import * as fs from 'fs';
import path from 'path';
import _ from 'lodash';
fs.mkdirSync('/etc/nginx/certs', { recursive: true });
class Cert {
public cert = new X509Certificate(
fs.readFileSync(path.join('/etc/nginx/certs', this.dir, 'fullchain.pem')),
......@@ -33,7 +34,7 @@ const certs = fs
.map((dir) => new Cert(dir))
.filter((cert) => cert.isNotExpired());
export function pickCert(domains: string[]) {
export async function pickCert(domains: string[]) {
const okCerts = certs.filter((cert) => cert.isOkWithDomains(domains));
if (!okCerts.length) {
return;
......
......@@ -3,6 +3,7 @@ import { Parser } from './parser';
import { getSiteNames } from './utility';
import md5 from 'nano-md5';
import fs from 'fs';
import { addSignCert, runSignCert } from './acme';
export interface SiteHttps {
ports: string[];
......@@ -100,8 +101,16 @@ function createUpstream(domain: string, urlInputs: string[], extra: string[]) {
return urlInputs[0];
}
const name = `upstream_${domain.replace(/[^a-zA-Z0-9]/g, '_')}`;
upstreams.push({ name, servers: urls.map((url) => `${url.host + (url.hash ? url.hash.replace(/[#&]/g, ' ') : '')}`), extra });
return `${urls[0].protocol}//${name}${urlInputs[0].slice(urls[0].origin.length).replace(urls[0].hash, '')}`;
upstreams.push({
name,
servers: urls.map(
(url) => `${url.host + (url.hash ? url.hash.replace(/[#&]/g, ' ') : '')}`,
),
extra,
});
return `${urls[0].protocol}//${name}${urlInputs[0]
.slice(urls[0].origin.length)
.replace(urls[0].hash, '')}`;
}
async function getSiteData(
......@@ -115,13 +124,14 @@ async function getSiteData(
const httpsCert = parser.getString('HTTPS');
const domains = hostname.split('+');
if (httpsCert !== '0' && httpsCert !== 'false') {
const cert =
!httpsCert ||
httpsCert === '1' ||
httpsCert === 'true' ||
httpsCert === 'auto'
? pickCert(domains)
: httpsCert;
const cert = httpsCert?.startsWith('acme://')
? addSignCert(domains, httpsCert)
: !httpsCert ||
httpsCert === '1' ||
httpsCert === 'true' ||
httpsCert === 'auto'
? await pickCert(domains)
: httpsCert;
if (cert) {
if (port) {
ports.pop();
......@@ -167,7 +177,11 @@ async function getSiteData(
const sni = parser.getString('SNI');
specificRenderData = {
proxy: true,
upstream: createUpstream(domain, targetUrlInputs, parser.getArray('UPSTREAM_EXTRA')),
upstream: createUpstream(
domain,
targetUrlInputs,
parser.getArray('UPSTREAM_EXTRA'),
),
noVerifyCerts: parser.getBoolean('NO_VERIFY_CERTS'),
noBuffer: parser.getBoolean('NO_BUFFER'),
sni: sni === '1',
......@@ -214,6 +228,10 @@ export async function getData(
input: Record<string, string> = process.env,
): Promise<RenderData> {
const parser = new Parser('', input);
const sites = await Promise.all(
getSiteNames().map((domain) => getSiteData(domain, input)),
);
await runSignCert();
return {
purgeAllowed: parser.getArray('PURGE_ALLOWED'),
externalRealIp: parser.getBoolean('EXTERNAL_REAL_IP'),
......@@ -226,9 +244,7 @@ export async function getData(
ticketKeyPath:
parser.getString('TICKET_KEY_PATH') || '/etc/nginx/generated/ticket.key',
certsPath: parser.getString('CERTS_PATH') || '/etc/nginx/certs',
sites: await Promise.all(
getSiteNames().map((domain) => getSiteData(domain, input)),
),
sites,
upstreams,
httpExtra: parser.getArray('HTTP_EXTRA'),
nginxExtra: parser.getArray('NGINX_EXTRA'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment