feat: custom certificate

This commit is contained in:
Andras Bacsai
2022-09-21 15:48:32 +02:00
parent 86ac6461d1
commit 90e639f119
30 changed files with 1000 additions and 367 deletions

View File

@@ -3,6 +3,7 @@ import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
@@ -31,6 +32,7 @@ prisma.setting.findFirst().then(async (settings) => {
logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true
});
const schema = {
type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
@@ -88,13 +90,13 @@ prisma.setting.findFirst().then(async (settings) => {
return reply.status(200).sendFile('index.html');
});
}
fastify.register(multipart, { limits: { fileSize: 100000 } });
fastify.register(autoLoad, {
dir: join(__dirname, 'plugins')
});
fastify.register(autoLoad, {
dir: join(__dirname, 'routes')
});
fastify.register(cookie)
fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => {
@@ -145,11 +147,15 @@ prisma.setting.findFirst().then(async (settings) => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10)
// checkProxies
// checkProxies and checkFluentBit
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
}, 10000)
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
}, 2000)
// cleanupPrismaEngines
// setInterval(async () => {
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines")

View File

@@ -1,8 +1,9 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
@@ -39,6 +40,46 @@ async function autoUpdater() {
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copySSLCertificates() {
try {
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.team.map(t => t.id)).flat()
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: teamIds } } } } })
for (const destination of destinations) {
if (destination.remoteEngine) {
// TODO: copy certificates to remote engine
} else {
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await asyncExecShell(`docker exec coolify-proxy sh -c 'mkdir -p /etc/traefik/acme/custom/'`)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
await fs.rm(`/tmp/${id}-key.pem`)
await fs.rm(`/tmp/${id}-cert.pem`)
}
}
}
} catch (error) {
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
@@ -215,6 +256,14 @@ async function cleanupStorage() {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
await copySSLCertificates();
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true

View File

@@ -1,6 +1,9 @@
import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types';
import pump from 'pump'
import fs from 'fs'
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
export interface Update {
Body: { latestVersion: string }

View File

@@ -1,8 +1,9 @@
import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) {
@@ -16,8 +17,16 @@ export async function listAllSettings(request: FastifyRequest) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
}
}
const certificates = await prisma.certificate.findMany({ where: { team: { every: { id: teamId } } } })
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt })
}
return {
settings,
certificates: cns,
sshKeys: unencryptedKeys
}
} catch ({ status, message }) {
@@ -118,7 +127,7 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
return errorHandler({ status, message })
}
}
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.sshKey.delete({ where: { id } })
@@ -126,4 +135,14 @@ export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply:
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await prisma.certificate.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,21 +1,58 @@
import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { X509Certificate } from 'node:crypto';
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, getCertificates, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listAllSettings(request));
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify()
})
fastify.get('/', async (request) => await listAllSettings(request));
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post('/upload', async (request) => {
try {
const teamId = request.user.teamId;
const certificates = await prisma.certificate.findMany({})
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push(x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''))
}
const parts = await request.files()
let key = null
let cert = null
for await (const part of parts) {
const name = part.fieldname
if (name === 'key') key = (await part.toBuffer()).toString()
if (name === 'cert') cert = (await part.toBuffer()).toString()
}
const x509 = new X509Certificate(cert);
const cn = x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', '')
if (cns.includes(cn)) {
throw {
message: `A certificate with ${cn} common name already exists.`
}
}
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
return { message: 'Certificated uploaded' }
} catch ({ status, message }) {
return errorHandler({ status, message });
}
});
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
// fastify.get('/certificates', async (request) => await getCertificates(request))
};
export default root;

View File

@@ -41,4 +41,9 @@ export interface DeleteSSHKey {
Body: {
id: string
}
}
export interface OnlyIdInBody {
Body: {
id: string
}
}

View File

@@ -178,7 +178,19 @@ function configureMiddleware(
export async function traefikConfiguration(request, reply) {
try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany()
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = {
tls: {
certificates: parsedCertificates
},
http: {
routers: {},
services: {},