feat: ssl certificate sets custom ssl for applications

This commit is contained in:
Andras Bacsai
2022-09-23 15:21:19 +02:00
parent f9d94fa660
commit 4abe9c6fb2
10 changed files with 102 additions and 45 deletions

View File

@@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -172,6 +172,7 @@ model ApplicationSettings {
isBot Boolean @default(false) isBot Boolean @default(false)
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])

View File

@@ -60,40 +60,49 @@ async function copySSLCertificates() {
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } }) const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const destination of destinations) { for (const destination of destinations) {
if (destination.remoteEngine) { if (destination.remoteEngine) {
const { id: dockerId, remoteIpAddress, remoteVerified } = destination const { id: dockerId, remoteIpAddress, remoteVerified } = destination
if (!remoteVerified) { if (!remoteVerified) {
continue; continue;
} }
// TODO: copy certificates to remote engine
for (const certificate of certificates) { for (const certificate of certificates) {
const { id, key, cert } = certificate try {
const decryptedKey = decrypt(key) const { id, key, cert } = certificate
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert) await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`) await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
await fs.rm(`/tmp/${id}-key.pem`) await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await fs.rm(`/tmp/${id}-cert.pem`) await fs.rm(`/tmp/${id}-key.pem`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) await fs.rm(`/tmp/${id}-cert.pem`)
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-key.pem` }) await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-cert.pem` }) await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-key.pem` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/ && rm /tmp/${id}-cert.pem` })
} catch (error) {
console.log('Error copying SSL certificates to remote engine', error)
}
} }
} else { } else {
for (const certificate of certificates) { for (const certificate of certificates) {
const { id, key, cert } = certificate try {
const decryptedKey = decrypt(key) const { id, key, cert } = certificate
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`) const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert) await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`) await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`) await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await fs.rm(`/tmp/${id}-key.pem`) await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
await fs.rm(`/tmp/${id}-cert.pem`) await fs.rm(`/tmp/${id}-key.pem`)
await fs.rm(`/tmp/${id}-cert.pem`)
} catch (error) {
console.log('Error copying SSL certificates to remote engine', error)
}
} }
} }
} }
} catch (error) { } catch (error) {
console.log('Error copying SSL certificates', error)
} }
} }
async function checkProxies() { async function checkProxies() {

View File

@@ -321,17 +321,12 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) { export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body
// const isDouble = await checkDoubleBranch(branch, projectId);
// if (isDouble && autodeploy) {
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
// throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
// }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
return reply.code(201).send(); return reply.code(201).send();
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@@ -26,7 +26,7 @@ export interface SaveApplication extends OnlyId {
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean }; Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };

View File

@@ -45,6 +45,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
} }
} }
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } }) await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
await prisma.applicationSettings.updateMany({ where: { application: { AND: [{ fqdn: { endsWith: cn } }, { fqdn: { startsWith: 'https' } }] } }, data: { isCustomSSL: true } })
return { message: 'Certificated uploaded' } return { message: 'Certificated uploaded' }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });

View File

@@ -6,7 +6,7 @@ import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types"; import { OnlyId } from "../../../types";
function configureMiddleware( function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL },
traefik traefik
) { ) {
if (isHttps) { if (isHttps) {
@@ -55,7 +55,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -66,7 +66,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -99,7 +99,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -179,7 +179,7 @@ function configureMiddleware(
export async function traefikConfiguration(request, reply) { export async function traefikConfiguration(request, reply) {
try { try {
const sslpath = '/etc/traefik/acme/custom'; const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany() const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } })
let parsedCertificates = [] let parsedCertificates = []
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({
@@ -236,7 +236,7 @@ export async function traefikConfiguration(request, reply) {
port, port,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts } settings: { previews, dualCerts, isCustomSSL }
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
@@ -256,7 +256,8 @@ export async function traefikConfiguration(request, reply) {
isRunning, isRunning,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
if (previews) { if (previews) {
@@ -279,7 +280,8 @@ export async function traefikConfiguration(request, reply) {
nakedDomain, nakedDomain,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
} }
@@ -547,7 +549,7 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
const { id } = request.params const { id } = request.params
try { try {
const sslpath = '/etc/traefik/acme/custom'; const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany({ where: { team: { destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } })
let parsedCertificates = [] let parsedCertificates = []
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({

View File

@@ -132,7 +132,7 @@
<ellipse cx="12" cy="6" rx="8" ry="3" /> <ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" /> <path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" /> <path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>Peristent Volumes</a </svg>Persistent Volumes</a
> >
</li> </li>
<li <li

View File

@@ -61,12 +61,14 @@
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let isCustomSSL = application.settings.isCustomSSL;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot; let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isHttps = application.fqdn && application.fqdn.startsWith('https://');
let isNonWWWDomainOK = false; let isNonWWWDomainOK = false;
let isWWWDomainOK = false; let isWWWDomainOK = false;
@@ -92,7 +94,7 @@
if (window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
await handleSubmit(); await handleSubmit();
} }
await getBaseBuildImages(); await getBaseBuildImages();
}); });
async function getBaseBuildImages() { async function getBaseBuildImages() {
@@ -141,6 +143,9 @@
if (name === 'autodeploy') { if (name === 'autodeploy') {
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.isRunning) return; if ($status.application.isRunning) return;
isBot = !isBot; isBot = !isBot;
@@ -159,6 +164,7 @@
isBot, isBot,
autodeploy, autodeploy,
isDBBranching, isDBBranching,
isCustomSSL,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId
}); });
@@ -185,6 +191,9 @@
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
@@ -214,6 +223,11 @@
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
}); });
if (application.fqdn && application.fqdn.startsWith('https')) {
isHttps = true;
} else {
isHttps = false;
}
} catch (error) { } catch (error) {
//@ts-ignore //@ts-ignore
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) { if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
@@ -548,6 +562,18 @@
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isCustomSSL"
isCenter={false}
bind:setting={isCustomSSL}
title="Use Custom SSL Certificate"
description="Use Custom SSL Certificated added in the Settings/SSL Certificates section. <br><br>By default, the SSL certificate is generated automatically through Let's Encrypt"
on:click={() => changeSettings('isCustomSSL')}
/>
</div>
{/if}
{/if} {/if}
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@@ -769,4 +795,4 @@
</div> </div>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -41,7 +41,7 @@
} }
} }
async function deleteCertificate(id: string) { async function deleteCertificate(id: string) {
const sure = confirm('Are you sure you would like to delete this SSH key?'); const sure = confirm('Are you sure you would like to delete this SSL Certificate?');
if (sure) { if (sure) {
try { try {
if (!id) return; if (!id) return;
@@ -89,7 +89,7 @@
<div class="text-sm">No SSL Certificate found</div> <div class="text-sm">No SSL Certificate found</div>
{/if} {/if}
</div> </div>
{#if isModalActive} {#if isModalActive}
<input type="checkbox" id="my-modal" class="modal-toggle" /> <input type="checkbox" id="my-modal" class="modal-toggle" />
<div class="modal modal-bottom sm:modal-middle "> <div class="modal modal-bottom sm:modal-middle ">