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() { ...@@ -15,5 +15,6 @@ async function main() {
{ escape: (v) => v }, { escape: (v) => v },
), ),
); );
process.exit(0);
} }
main(); main();
This diff is collapsed.
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"dependencies": { "dependencies": {
"acme-client": "^5.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nano-md5": "^1.0.5" "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'; ...@@ -3,6 +3,7 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import _ from 'lodash'; import _ from 'lodash';
fs.mkdirSync('/etc/nginx/certs', { recursive: true });
class Cert { class Cert {
public cert = new X509Certificate( public cert = new X509Certificate(
fs.readFileSync(path.join('/etc/nginx/certs', this.dir, 'fullchain.pem')), fs.readFileSync(path.join('/etc/nginx/certs', this.dir, 'fullchain.pem')),
...@@ -33,7 +34,7 @@ const certs = fs ...@@ -33,7 +34,7 @@ const certs = fs
.map((dir) => new Cert(dir)) .map((dir) => new Cert(dir))
.filter((cert) => cert.isNotExpired()); .filter((cert) => cert.isNotExpired());
export function pickCert(domains: string[]) { export async function pickCert(domains: string[]) {
const okCerts = certs.filter((cert) => cert.isOkWithDomains(domains)); const okCerts = certs.filter((cert) => cert.isOkWithDomains(domains));
if (!okCerts.length) { if (!okCerts.length) {
return; return;
......
...@@ -3,6 +3,7 @@ import { Parser } from './parser'; ...@@ -3,6 +3,7 @@ import { Parser } from './parser';
import { getSiteNames } from './utility'; import { getSiteNames } from './utility';
import md5 from 'nano-md5'; import md5 from 'nano-md5';
import fs from 'fs'; import fs from 'fs';
import { addSignCert, runSignCert } from './acme';
export interface SiteHttps { export interface SiteHttps {
ports: string[]; ports: string[];
...@@ -100,8 +101,16 @@ function createUpstream(domain: string, urlInputs: string[], extra: string[]) { ...@@ -100,8 +101,16 @@ function createUpstream(domain: string, urlInputs: string[], extra: string[]) {
return urlInputs[0]; return urlInputs[0];
} }
const name = `upstream_${domain.replace(/[^a-zA-Z0-9]/g, '_')}`; 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 }); upstreams.push({
return `${urls[0].protocol}//${name}${urlInputs[0].slice(urls[0].origin.length).replace(urls[0].hash, '')}`; 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( async function getSiteData(
...@@ -115,13 +124,14 @@ async function getSiteData( ...@@ -115,13 +124,14 @@ async function getSiteData(
const httpsCert = parser.getString('HTTPS'); const httpsCert = parser.getString('HTTPS');
const domains = hostname.split('+'); const domains = hostname.split('+');
if (httpsCert !== '0' && httpsCert !== 'false') { if (httpsCert !== '0' && httpsCert !== 'false') {
const cert = const cert = httpsCert?.startsWith('acme://')
!httpsCert || ? addSignCert(domains, httpsCert)
httpsCert === '1' || : !httpsCert ||
httpsCert === 'true' || httpsCert === '1' ||
httpsCert === 'auto' httpsCert === 'true' ||
? pickCert(domains) httpsCert === 'auto'
: httpsCert; ? await pickCert(domains)
: httpsCert;
if (cert) { if (cert) {
if (port) { if (port) {
ports.pop(); ports.pop();
...@@ -167,7 +177,11 @@ async function getSiteData( ...@@ -167,7 +177,11 @@ async function getSiteData(
const sni = parser.getString('SNI'); const sni = parser.getString('SNI');
specificRenderData = { specificRenderData = {
proxy: true, proxy: true,
upstream: createUpstream(domain, targetUrlInputs, parser.getArray('UPSTREAM_EXTRA')), upstream: createUpstream(
domain,
targetUrlInputs,
parser.getArray('UPSTREAM_EXTRA'),
),
noVerifyCerts: parser.getBoolean('NO_VERIFY_CERTS'), noVerifyCerts: parser.getBoolean('NO_VERIFY_CERTS'),
noBuffer: parser.getBoolean('NO_BUFFER'), noBuffer: parser.getBoolean('NO_BUFFER'),
sni: sni === '1', sni: sni === '1',
...@@ -214,6 +228,10 @@ export async function getData( ...@@ -214,6 +228,10 @@ export async function getData(
input: Record<string, string> = process.env, input: Record<string, string> = process.env,
): Promise<RenderData> { ): Promise<RenderData> {
const parser = new Parser('', input); const parser = new Parser('', input);
const sites = await Promise.all(
getSiteNames().map((domain) => getSiteData(domain, input)),
);
await runSignCert();
return { return {
purgeAllowed: parser.getArray('PURGE_ALLOWED'), purgeAllowed: parser.getArray('PURGE_ALLOWED'),
externalRealIp: parser.getBoolean('EXTERNAL_REAL_IP'), externalRealIp: parser.getBoolean('EXTERNAL_REAL_IP'),
...@@ -226,9 +244,7 @@ export async function getData( ...@@ -226,9 +244,7 @@ export async function getData(
ticketKeyPath: ticketKeyPath:
parser.getString('TICKET_KEY_PATH') || '/etc/nginx/generated/ticket.key', parser.getString('TICKET_KEY_PATH') || '/etc/nginx/generated/ticket.key',
certsPath: parser.getString('CERTS_PATH') || '/etc/nginx/certs', certsPath: parser.getString('CERTS_PATH') || '/etc/nginx/certs',
sites: await Promise.all( sites,
getSiteNames().map((domain) => getSiteData(domain, input)),
),
upstreams, upstreams,
httpExtra: parser.getArray('HTTP_EXTRA'), httpExtra: parser.getArray('HTTP_EXTRA'),
nginxExtra: parser.getArray('NGINX_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