verifyCanary, DEK safeguards
This commit is contained in:
parent
893cebc35f
commit
0b59ff6e07
5
.env
5
.env
@ -2,4 +2,7 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://
|
|||||||
SERVER1_PORT=5000
|
SERVER1_PORT=5000
|
||||||
SERVER2_PORT=5001
|
SERVER2_PORT=5001
|
||||||
SERVER3_PORT=5002
|
SERVER3_PORT=5002
|
||||||
IMG_TAG=16e01ab-202508071457
|
IMG_TAG=1d50efe-202508081233
|
||||||
|
|
||||||
|
ENV_NAME=dev
|
||||||
|
PROJECT=aptivaai-dev
|
@ -34,6 +34,7 @@ steps:
|
|||||||
'set -euo pipefail; \
|
'set -euo pipefail; \
|
||||||
PROJECT=aptivaai-dev; \
|
PROJECT=aptivaai-dev; \
|
||||||
ENV=staging; \
|
ENV=staging; \
|
||||||
|
ENV_NAME=staging; \
|
||||||
IMG_TAG=$(gcloud secrets versions access latest --secret=IMG_TAG --project=$PROJECT); \
|
IMG_TAG=$(gcloud secrets versions access latest --secret=IMG_TAG --project=$PROJECT); \
|
||||||
export IMG_TAG; \
|
export IMG_TAG; \
|
||||||
JWT_SECRET=$(gcloud secrets versions access latest --secret=JWT_SECRET_$ENV --project=$PROJECT); \
|
JWT_SECRET=$(gcloud secrets versions access latest --secret=JWT_SECRET_$ENV --project=$PROJECT); \
|
||||||
@ -87,16 +88,25 @@ steps:
|
|||||||
|
|
||||||
export FROM_SECRETS_MANAGER=true; \
|
export FROM_SECRETS_MANAGER=true; \
|
||||||
cd /home/jcoakley/aptiva-staging-app; \
|
cd /home/jcoakley/aptiva-staging-app; \
|
||||||
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH \
|
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,ENV,ENV_NAME,PROJECT \ \
|
||||||
docker compose pull; \
|
docker compose pull; \
|
||||||
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH \
|
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,ENV,ENV_NAME,PROJECT \ \
|
||||||
docker compose up -d --force-recreate --remove-orphans; \
|
docker compose up -d --force-recreate --remove-orphans; \
|
||||||
echo "✅ Staging stack refreshed with tag $IMG_TAG"'
|
echo "✅ Staging stack refreshed with tag $IMG_TAG"'
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
- STAGING_SSH_KEY
|
- STAGING_SSH_KEY
|
||||||
- STAGING_KNOWN_HOSTS
|
- STAGING_KNOWN_HOSTS
|
||||||
|
|
||||||
|
- name: security-scan
|
||||||
|
image: aquasec/trivy:0.52
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
REG=us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo
|
||||||
|
trivy image --exit-code 1 --severity CRITICAL $REG/server1:$IMG_TAG
|
||||||
|
trivy image --exit-code 1 --severity CRITICAL $REG/server2:$IMG_TAG
|
||||||
|
trivy image --exit-code 1 --severity CRITICAL $REG/server3:$IMG_TAG
|
||||||
|
trivy image --exit-code 1 --severity CRITICAL $REG/nginx:$IMG_TAG
|
||||||
|
|
||||||
when:
|
when:
|
||||||
event:
|
event: [push]
|
||||||
- push
|
|
||||||
|
@ -7,4 +7,4 @@ RUN rm /etc/nginx/nginx.conf
|
|||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# Copy React build output into staging app's build folder
|
# Copy React build output into staging app's build folder
|
||||||
COPY build/ /usr/share/nginx/html
|
COPY build/ /usr/share/nginx/html
|
@ -1,4 +1,7 @@
|
|||||||
FROM node:20-bullseye AS base
|
FROM node:20-bullseye AS base
|
||||||
|
|
||||||
|
RUN groupadd -r app && useradd -r -g app app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# ---- native build deps ----
|
# ---- native build deps ----
|
||||||
@ -13,4 +16,8 @@ COPY public/ /app/public/
|
|||||||
RUN npm ci --unsafe-perm
|
RUN npm ci --unsafe-perm
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /run/secrets && chown -R app:app /run/secrets
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
CMD ["node", "backend/server1.js"]
|
CMD ["node", "backend/server1.js"]
|
@ -1,4 +1,7 @@
|
|||||||
FROM node:20-bullseye AS base
|
FROM node:20-bullseye AS base
|
||||||
|
|
||||||
|
RUN groupadd -r app && useradd -r -g app app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# ---- native build deps ----
|
# ---- native build deps ----
|
||||||
@ -13,4 +16,7 @@ COPY public/ /app/public/
|
|||||||
RUN npm ci --unsafe-perm
|
RUN npm ci --unsafe-perm
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /run/secrets && chown -R app:app /run/secrets
|
||||||
|
|
||||||
|
USER app
|
||||||
CMD ["node", "backend/server2.js"]
|
CMD ["node", "backend/server2.js"]
|
@ -1,20 +1,23 @@
|
|||||||
# ---- Dockerfile.server3 (fixed) ------------------------------
|
FROM node:20-bullseye AS base
|
||||||
FROM node:20-bullseye
|
|
||||||
|
RUN groupadd -r app && useradd -r -g app app
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 1. native build dependencies + curl
|
# ---- native build deps ----
|
||||||
RUN apt-get update -y && \
|
RUN apt-get update -y && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
build-essential python3 pkg-config curl && \
|
build-essential python3 pkg-config && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
# 2. node deps
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev --unsafe-perm
|
COPY public/ /app/public/
|
||||||
|
RUN npm ci --unsafe-perm
|
||||||
# 3. static assets & source
|
|
||||||
COPY public/ /app/public/
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /run/secrets && chown -R app:app /run/secrets
|
||||||
|
|
||||||
|
USER app
|
||||||
CMD ["node", "backend/server3.js"]
|
CMD ["node", "backend/server3.js"]
|
||||||
|
|
||||||
|
@ -1,61 +1,88 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import axios from 'axios';
|
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import jwt from 'jsonwebtoken'; // For token-based authentication
|
import jwt from 'jsonwebtoken'; // For token-based authentication
|
||||||
import { initEncryption } from './shared/crypto/encryption.js';
|
import { initEncryption, encrypt, decrypt, verifyCanary, SENTINEL } from './shared/crypto/encryption.js';
|
||||||
|
|
||||||
import pool from './config/mysqlPool.js'; // adjust path if needed
|
import pool from './config/mysqlPool.js'; // adjust path if needed
|
||||||
|
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
|
|
||||||
|
|
||||||
|
const CANARY_SQL = `
|
||||||
|
CREATE TABLE IF NOT EXISTS encryption_canary (
|
||||||
|
id TINYINT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const rootPath = path.resolve(__dirname, '..'); // Up one level
|
const rootPath = path.resolve(__dirname, '..'); // Up one level
|
||||||
const env = process.env.NODE_ENV?.trim() || 'development';
|
const env = process.env.NODE_ENV?.trim() || 'development';
|
||||||
const stage = env === 'staging' ? 'development' : env;
|
|
||||||
const envPath = path.resolve(rootPath, `.env.${env}`);
|
const envPath = path.resolve(rootPath, `.env.${env}`);
|
||||||
dotenv.config({ path: envPath }); // Load .env file
|
dotenv.config({ path: envPath }); // Load .env file
|
||||||
|
|
||||||
// Grab secrets and config from ENV
|
// Grab secrets and config from ENV
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
const {
|
||||||
const DB_HOST = process.env.DB_HOST || '127.0.0.1';
|
JWT_SECRET,
|
||||||
const DB_PORT = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306;
|
CORS_ALLOWED_ORIGINS,
|
||||||
const DB_USER = process.env.DB_USER || 'sqluser';
|
SERVER1_PORT = 5000
|
||||||
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
} = process.env;
|
||||||
const DB_NAME = process.env.DB_NAME || 'user_profile_db';
|
|
||||||
|
|
||||||
if (!JWT_SECRET) {
|
if (!JWT_SECRET) {
|
||||||
console.error('FATAL: JWT_SECRET missing – aborting startup');
|
console.error('FATAL: JWT_SECRET missing – aborting startup');
|
||||||
process.exit(1); // container exits, Docker marks it unhealthy
|
process.exit(1); // container exits, Docker marks it unhealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
await initEncryption();
|
if (!CORS_ALLOWED_ORIGINS) {
|
||||||
// Test a quick query (optional)
|
console.error('FATAL: CORS_ALLOWED_ORIGINS missing – aborting startup');
|
||||||
try {
|
|
||||||
const [rows] = await pool.query('SELECT 1');
|
|
||||||
console.log('Connected to MySQL user_profile_db');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error connecting to MySQL user_profile_db:', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const PORT = process.env.SERVER1_PORT || 5000;
|
|
||||||
|
|
||||||
/* ─── Require critical env vars ───────────────────────────────── */
|
|
||||||
if (!process.env.CORS_ALLOWED_ORIGINS) {
|
|
||||||
console.error('FATAL CORS_ALLOWED_ORIGINS is not set'); // eslint-disable-line
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── unwrap / verify DEK before we serve requests ────────────── */
|
||||||
|
await initEncryption();
|
||||||
|
|
||||||
|
try {
|
||||||
|
/* quick connectivity smoke‑test (optional) */
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
|
||||||
|
/* ① ensure table exists */
|
||||||
|
await pool.query(CANARY_SQL);
|
||||||
|
|
||||||
|
/* ② insert sentinel on first run */
|
||||||
|
await pool.query(
|
||||||
|
'INSERT IGNORE INTO encryption_canary (id, value) VALUES (1, ?)',
|
||||||
|
[encrypt(SENTINEL)]
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ③ read back & verify */
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT value FROM encryption_canary WHERE id = 1 LIMIT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
const plaintext = decrypt(rows[0]?.value || '');
|
||||||
|
if (plaintext !== SENTINEL) {
|
||||||
|
throw new Error('DEK mismatch with database sentinel');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ENCRYPT] DEK verified against canary – proceeding');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('FATAL:', err.message || err);
|
||||||
|
process.exit(1); // container restarts → alert
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ────────────────────────────────────────────────────────────────
|
||||||
|
Express app & middleware
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.SERVER1_PORT || 5000;
|
||||||
|
|
||||||
/* ─── Allowed origins for CORS (comma-separated in env) ──────── */
|
/* ─── Allowed origins for CORS (comma-separated in env) ──────── */
|
||||||
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
|
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
|
||||||
.split(',')
|
.split(',')
|
||||||
@ -73,7 +100,15 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
app.get('/healthz', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await verifyCanary(pool); // throws if bad
|
||||||
|
return res.type('text').send('OK'); // cheap 200 OK
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[HEALTHZ]', e.message);
|
||||||
|
return res.status(500).type('text').send('FAIL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Enable CORS with dynamic origin checking
|
// Enable CORS with dynamic origin checking
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -12,6 +12,7 @@ import path from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { open } from 'sqlite';
|
import { open } from 'sqlite';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
|
import pool from './config/mysqlPool.js'; // adjust path if needed
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import chatFreeEndpoint from "./utils/chatFreeEndpoint.js";
|
import chatFreeEndpoint from "./utils/chatFreeEndpoint.js";
|
||||||
@ -19,7 +20,8 @@ import { OpenAI } from 'openai';
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import authenticateUser from './utils/authenticateUser.js';
|
import authenticateUser from './utils/authenticateUser.js';
|
||||||
import { vectorSearch } from "./utils/vectorSearch.js";
|
import { vectorSearch } from "./utils/vectorSearch.js";
|
||||||
import { initEncryption } from './shared/crypto/encryption.js';
|
import { initEncryption, verifyCanary } from './shared/crypto/encryption.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Basic file init ---
|
// --- Basic file init ---
|
||||||
@ -58,13 +60,23 @@ const chatLimiter = rateLimit({
|
|||||||
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
const institutionData = JSON.parse(fs.readFileSync(INSTITUTION_DATA_PATH, 'utf8'));
|
||||||
|
|
||||||
await initEncryption();
|
await initEncryption();
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
await verifyCanary(pool);
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.SERVER2_PORT || 5001;
|
const PORT = process.env.SERVER2_PORT || 5001;
|
||||||
|
|
||||||
// at top of backend/server.js (do once per server codebase)
|
// at top of backend/server.js (do once per server codebase)
|
||||||
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
app.get('/healthz', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await verifyCanary(pool); // << just pass the pool
|
||||||
|
return res.type('text').send('OK');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[HEALTHZ]', e.message);
|
||||||
|
return res.status(500).type('text').send('FAIL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* DB connections
|
* DB connections
|
||||||
|
@ -15,17 +15,20 @@ import mammoth from 'mammoth';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import pkg from 'pdfjs-dist';
|
import pkg from 'pdfjs-dist';
|
||||||
import db from './config/mysqlPool.js';
|
import pool from './config/mysqlPool.js'; // adjust path if needed
|
||||||
|
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { createReminder } from './utils/smsService.js';
|
import { createReminder } from './utils/smsService.js';
|
||||||
|
|
||||||
import { initEncryption } from './shared/crypto/encryption.js';
|
import { initEncryption, verifyCanary } from './shared/crypto/encryption.js';
|
||||||
import { hashForLookup } from './shared/crypto/encryption.js';
|
import { hashForLookup } from './shared/crypto/encryption.js';
|
||||||
|
|
||||||
await initEncryption();
|
await initEncryption();
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
await verifyCanary(pool);
|
||||||
|
|
||||||
import './jobs/reminderCron.js';
|
import './jobs/reminderCron.js';
|
||||||
import { cacheSummary } from "./utils/ctxCache.js";
|
import { cacheSummary } from "./utils/ctxCache.js";
|
||||||
|
|
||||||
@ -69,7 +72,15 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// at top of backend/server.js (do once per server codebase)
|
// at top of backend/server.js (do once per server codebase)
|
||||||
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
app.get('/healthz', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await verifyCanary(pool); // << just pass the pool
|
||||||
|
return res.type('text').send('OK');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[HEALTHZ]', e.message);
|
||||||
|
return res.status(500).type('text').send('FAIL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function internalFetch(req, urlPath, opts = {}) {
|
function internalFetch(req, urlPath, opts = {}) {
|
||||||
return fetch(`${API_BASE}${urlPath}`, {
|
return fetch(`${API_BASE}${urlPath}`, {
|
||||||
@ -256,9 +267,6 @@ const authenticatePremiumUser = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pool = db;
|
|
||||||
|
|
||||||
|
|
||||||
/** ------------------------------------------------------------------
|
/** ------------------------------------------------------------------
|
||||||
* Returns the user’s stripe_customer_id (or null) given req.id.
|
* Returns the user’s stripe_customer_id (or null) given req.id.
|
||||||
* Creates a new Stripe Customer & saves it if missing.
|
* Creates a new Stripe Customer & saves it if missing.
|
||||||
|
@ -1,143 +1,178 @@
|
|||||||
/* ────────────────────────────────────────────────────────────────
|
/* ────────────────────────────────────────────────────────────────
|
||||||
AES‑GCM field‑level encryption helper backed by Google Cloud KMS
|
AES‑GCM field‑level encryption helper backed by Google Cloud KMS
|
||||||
---------------------------------------------------------------- */
|
---------------------------------------------------------------- */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
randomBytes,
|
randomBytes, createCipheriv, createDecipheriv,
|
||||||
createCipheriv,
|
createHmac, createHash,
|
||||||
createDecipheriv,
|
|
||||||
createHmac
|
|
||||||
} from 'crypto';
|
} from 'crypto';
|
||||||
import { KeyManagementServiceClient } from '@google-cloud/kms';
|
import { KeyManagementServiceClient } from '@google-cloud/kms';
|
||||||
import { open as fsOpen, readFile, mkdir, writeFile } from 'fs/promises';
|
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
||||||
|
import {
|
||||||
|
open as fsOpen, readFile, mkdir,
|
||||||
|
} from 'fs/promises';
|
||||||
import { constants as FS } from 'fs';
|
import { constants as FS } from 'fs';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
/* ── constants ─────────────────────────────────────────────── */
|
/* ── constants ─────────────────────────────────────────────── */
|
||||||
const ALGO = 'aes-256-gcm'; // symmetric cipher
|
const ALGO = 'aes-256-gcm';
|
||||||
const IV_LEN = 12; // 96‑bit nonce for GCM
|
const IV_LEN = 12; // 96‑bit nonce
|
||||||
const TAG_LEN = 16; // 128‑bit auth‑tag
|
const TAG_LEN = 16; // 128‑bit auth‑tag
|
||||||
const MAGIC = 'gcm:'; // prefix to mark ciphertext
|
const MAGIC = 'gcm:'; // ciphertext prefix
|
||||||
|
|
||||||
/* ── env config (injected via docker‑compose) ──────────────── */
|
/* ── env config (docker‑compose) ────────────────────────────── */
|
||||||
const KMS_KEY = (process.env.KMS_KEY_NAME || '').trim(); // projects/*/locations/*/keyRings/*/cryptoKeys/*
|
const KMS_KEY = (process.env.KMS_KEY_NAME || '').trim();
|
||||||
const EDEK_PATH = (process.env.DEK_PATH || '').trim(); // e.g. /run/secrets/dev/dek.enc
|
const EDEK_PATH = (process.env.DEK_PATH || '').trim(); // /run/secrets/<env>/dek.enc
|
||||||
|
const ENV_NAME = (process.env.ENV_NAME || 'dev').trim(); // dev | staging | prod
|
||||||
|
const PROJECT = (process.env.PROJECT || 'aptivaai-dev').trim();
|
||||||
|
|
||||||
if (!EDEK_PATH) {
|
if (!EDEK_PATH) {
|
||||||
console.error('FATAL: DEK_PATH env var is unset — check deploy script');
|
console.error('FATAL: DEK_PATH env var is unset — check deploy script');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
const FPR_PATH = path.join(path.dirname(EDEK_PATH), 'dek.fpr');
|
||||||
|
const SM_SECRET = `WRAPPED_DEK_${ENV_NAME.toUpperCase()}`;
|
||||||
|
|
||||||
let dek; // in‑memory data‑encryption‑key
|
let dek; // plaintext DEK in memory
|
||||||
let initPromise = null;
|
let initPromise; // so parallel callers wait only once
|
||||||
|
|
||||||
|
/* ── helper: upload a backup copy to Secret Manager ─────────── */
|
||||||
|
async function backupToSecretManager (ciphertext /* Buffer */) {
|
||||||
|
const sm = new SecretManagerServiceClient();
|
||||||
|
|
||||||
|
/* ensure the secret exists – idempotent */
|
||||||
|
try {
|
||||||
|
await sm.getSecret({ name: `projects/${PROJECT}/secrets/${SM_SECRET}` });
|
||||||
|
} catch {
|
||||||
|
await sm.createSecret({
|
||||||
|
parent : `projects/${PROJECT}`,
|
||||||
|
secretId : SM_SECRET,
|
||||||
|
secret : { replication: { automatic: {} } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* add a new version (binary) */
|
||||||
|
await sm.addSecretVersion({
|
||||||
|
parent : `projects/${PROJECT}/secrets/${SM_SECRET}`,
|
||||||
|
payload : { data: ciphertext },
|
||||||
|
});
|
||||||
|
console.log(`[ENCRYPT] ⬆️ copied dek.enc to Secret Manager (${SM_SECRET})`);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────────
|
||||||
One‑time DEK unwrap (or generate + wrap)
|
One‑time DEK unwrap / generate + fingerprint validation
|
||||||
──────────────────────────────────────────────────────────── */
|
──────────────────────────────────────────────────────────── */
|
||||||
export async function initEncryption () {
|
export async function initEncryption () {
|
||||||
/* fast path ─ already done in this process */
|
if (dek) return;
|
||||||
if (dek) return;
|
if (initPromise) return initPromise;
|
||||||
if (initPromise) return initPromise;
|
|
||||||
|
|
||||||
initPromise = (async () => {
|
initPromise = (async () => {
|
||||||
const kms = new KeyManagementServiceClient();
|
const kms = new KeyManagementServiceClient();
|
||||||
const dir = path.dirname(EDEK_PATH);
|
const dir = path.dirname(EDEK_PATH);
|
||||||
|
|
||||||
/* 1 ── try the happy path: read existing wrapped key */
|
/* 1 – optimistic: use existing wrapped key */
|
||||||
try {
|
try {
|
||||||
const edek = await readFile(EDEK_PATH);
|
const wrapped = await readFile(EDEK_PATH);
|
||||||
const [resp] = await kms.decrypt({ name: KMS_KEY, ciphertext: edek });
|
dek = (await kms.decrypt({ name: KMS_KEY, ciphertext: wrapped }))[0].plaintext;
|
||||||
dek = resp.plaintext;
|
|
||||||
process.env.DEBUG_ENCRYPTION === 'true'
|
|
||||||
&& console.log('[ENCRYPT] DEK unwrapped from KMS');
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== 'ENOENT') throw err; // genuine failure
|
if (err.code !== 'ENOENT') throw err; // genuine failure
|
||||||
}
|
|
||||||
|
|
||||||
/* 2 ── file does not exist ⇒ we’re in a race to create it */
|
/* 2 – first container wins: create DEK & wrap */
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
try {
|
|
||||||
/* exclusive create – succeeds for exactly ONE container */
|
|
||||||
const fh = await fsOpen(EDEK_PATH, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL, 0o600);
|
|
||||||
try {
|
try {
|
||||||
dek = randomBytes(32); // 256‑bit AES key
|
const fh = await fsOpen(EDEK_PATH, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL, 0o600);
|
||||||
const [wrapResp] = await kms.encrypt({ name: KMS_KEY, plaintext: dek });
|
try {
|
||||||
await fh.writeFile(wrapResp.ciphertext);
|
dek = randomBytes(32);
|
||||||
process.env.DEBUG_ENCRYPTION === 'true'
|
const [resp] = await kms.encrypt({ name: KMS_KEY, plaintext: dek });
|
||||||
&& console.log('[ENCRYPT] New DEK generated & wrapped');
|
const wrapped = resp.ciphertext; // Buffer
|
||||||
} finally {
|
|
||||||
await fh.close();
|
|
||||||
}
|
|
||||||
return; // we are the winner
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'EEXIST') throw err; // unexpected error
|
|
||||||
/* another container won – fall through to retry loop */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3 ── wait until the winner finishes writing, then read */
|
await fh.writeFile(wrapped); // primary copy
|
||||||
const maxRetries = 30; // ~3 s total
|
backupToSecretManager(wrapped) // async fire‑and‑forget
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
.catch(e => console.error('[ENCRYPT] backup failed:', e.message));
|
||||||
try {
|
} finally {
|
||||||
const edek = await readFile(EDEK_PATH);
|
await fh.close();
|
||||||
const [resp] = await kms.decrypt({ name: KMS_KEY, ciphertext: edek });
|
}
|
||||||
dek = resp.plaintext;
|
} catch (race) {
|
||||||
process.env.DEBUG_ENCRYPTION === 'true'
|
if (race.code !== 'EEXIST') throw race; // unexpected
|
||||||
&& console.log('[ENCRYPT] DEK unwrapped after retry');
|
/* another container already wrote the file */
|
||||||
return;
|
const wrapped = await readFile(EDEK_PATH);
|
||||||
} catch (err) {
|
dek = (await kms.decrypt({ name: KMS_KEY, ciphertext: wrapped }))[0].plaintext;
|
||||||
if (err.code !== 'ENOENT') throw err;
|
|
||||||
await new Promise(r => setTimeout(r, 100)); // back‑off 100 ms
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Timed out waiting for ${EDEK_PATH} to appear`);
|
|
||||||
|
/* 3 – fingerprint validation */
|
||||||
|
const fp = createHash('sha256').update(dek).digest('hex').slice(0, 16);
|
||||||
|
try {
|
||||||
|
const onDisk = (await readFile(FPR_PATH, 'utf8')).trim();
|
||||||
|
if (onDisk !== fp) throw new Error(`fingerprint mismatch: ${onDisk} ≠ ${fp}`);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') throw err; // real error
|
||||||
|
|
||||||
|
/* write fingerprint (first‑writer wins) */
|
||||||
|
await fsOpen(FPR_PATH, FS.O_WRONLY | FS.O_CREAT | FS.O_EXCL, 0o444)
|
||||||
|
.then(fh => fh.writeFile(fp + '\n').finally(() => fh.close()))
|
||||||
|
.catch(async (dup) => {
|
||||||
|
if (dup.code !== 'EEXIST') throw dup;
|
||||||
|
const onDisk = (await readFile(FPR_PATH, 'utf8')).trim();
|
||||||
|
if (onDisk !== fp) throw new Error(`fingerprint mismatch: ${onDisk} ≠ ${fp}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.DEBUG_ENCRYPTION === 'true'
|
||||||
|
&& console.log('[ENCRYPT] DEK ready – fp', fp);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return initPromise;
|
return initPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────
|
export const SENTINEL = 'aptiva-canary-v1';
|
||||||
Symmetric encryption helpers
|
|
||||||
──────────────────────────────────────────────────────────── */
|
/* ── optional DB sentinel helper ────────────────────────────── */
|
||||||
|
export async function verifyCanary(pool) {
|
||||||
|
// nothing to verify yet → first ever launch before server1 seeded it
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT value FROM encryption_canary WHERE id = 1 LIMIT 1'
|
||||||
|
);
|
||||||
|
if (!rows || rows.length === 0) return;
|
||||||
|
|
||||||
|
const val = rows[0].value;
|
||||||
|
let plain;
|
||||||
|
try {
|
||||||
|
plain = decrypt(val);
|
||||||
|
} catch {
|
||||||
|
throw new Error('DEK cannot decrypt sentinel row');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plain !== SENTINEL) {
|
||||||
|
throw new Error('DEK mismatch: sentinel value differs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── crypto helpers (encrypt / decrypt / hash) ─────────────── */
|
||||||
export function encrypt (plain) {
|
export function encrypt (plain) {
|
||||||
if (plain == null) return null; // leave NULLs untouched
|
if (plain == null || isEncrypted(plain)) return plain;
|
||||||
if (isEncrypted(plain)) return plain; // already encrypted
|
const iv = randomBytes(IV_LEN);
|
||||||
|
|
||||||
const iv = randomBytes(IV_LEN);
|
|
||||||
const cipher = createCipheriv(ALGO, dek, iv);
|
const cipher = createCipheriv(ALGO, dek, iv);
|
||||||
const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
return MAGIC + Buffer.concat([iv, tag, ct]).toString('base64');
|
return MAGIC + Buffer.concat([iv, tag, ct]).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt (val) {
|
export function decrypt (val) {
|
||||||
if (!isEncrypted(val)) return val; // fast‑exit for plain text
|
if (!isEncrypted(val)) return val;
|
||||||
|
|
||||||
const buf = Buffer.from(val.slice(MAGIC.length), 'base64');
|
const buf = Buffer.from(val.slice(MAGIC.length), 'base64');
|
||||||
const iv = buf.subarray(0, IV_LEN);
|
const iv = buf.subarray(0, IV_LEN);
|
||||||
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
||||||
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
const ct = buf.subarray(IV_LEN + TAG_LEN);
|
||||||
|
|
||||||
const decipher = createDecipheriv(ALGO, dek, iv);
|
const decipher = createDecipheriv(ALGO, dek, iv);
|
||||||
decipher.setAuthTag(tag);
|
decipher.setAuthTag(tag);
|
||||||
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────
|
|
||||||
Deterministic HMAC (for indexed look‑ups, optional)
|
|
||||||
──────────────────────────────────────────────────────────── */
|
|
||||||
export function hashForLookup (plain) {
|
export function hashForLookup (plain) {
|
||||||
if (plain == null) return null;
|
if (plain == null) return null;
|
||||||
const h = createHmac('sha256', dek).update(String(plain)).digest('hex');
|
return createHmac('sha256', dek).update(String(plain)).digest('hex');
|
||||||
return h; // 64‑char hex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────
|
|
||||||
Utility: is this value an AES‑GCM ciphertext we created?
|
|
||||||
──────────────────────────────────────────────────────────── */
|
|
||||||
export function isEncrypted (val) {
|
export function isEncrypted (val) {
|
||||||
return typeof val === 'string'
|
return typeof val === 'string'
|
||||||
&& val.startsWith(MAGIC)
|
&& val.startsWith(MAGIC)
|
||||||
|
@ -12,8 +12,18 @@ services:
|
|||||||
server1:
|
server1:
|
||||||
<<: *with-env
|
<<: *with-env
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${IMG_TAG}
|
||||||
|
user: "1000:1000"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
expose: ["${SERVER1_PORT}"]
|
expose: ["${SERVER1_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
ENV_NAME: ${ENV_NAME}
|
||||||
|
PROJECT: ${PROJECT}
|
||||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
DEK_PATH: ${DEK_PATH}
|
DEK_PATH: ${DEK_PATH}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
@ -31,7 +41,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
- dek-vol:/run/secrets
|
- dek-vol:/run/secrets:rw
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER1_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -42,8 +52,18 @@ services:
|
|||||||
server2:
|
server2:
|
||||||
<<: *with-env
|
<<: *with-env
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${IMG_TAG}
|
||||||
|
user: "1000:1000"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
expose: ["${SERVER2_PORT}"]
|
expose: ["${SERVER2_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
ENV_NAME: ${ENV_NAME}
|
||||||
|
PROJECT: ${PROJECT}
|
||||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
DEK_PATH: ${DEK_PATH}
|
DEK_PATH: ${DEK_PATH}
|
||||||
ONET_USERNAME: ${ONET_USERNAME}
|
ONET_USERNAME: ${ONET_USERNAME}
|
||||||
@ -55,6 +75,9 @@ services:
|
|||||||
DB_USER: ${DB_USER}
|
DB_USER: ${DB_USER}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
DB_NAME: ${DB_NAME}
|
DB_NAME: ${DB_NAME}
|
||||||
|
DB_SSL_CERT: ${DB_SSL_CERT}
|
||||||
|
DB_SSL_KEY: ${DB_SSL_KEY}
|
||||||
|
DB_SSL_CA: ${DB_SSL_CA}
|
||||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
|
||||||
SALARY_DB_PATH: /app/salary_info.db
|
SALARY_DB_PATH: /app/salary_info.db
|
||||||
FROM_SECRETS_MANAGER: ${FROM_SECRETS_MANAGER}
|
FROM_SECRETS_MANAGER: ${FROM_SECRETS_MANAGER}
|
||||||
@ -62,7 +85,7 @@ services:
|
|||||||
- ./public:/app/public:ro
|
- ./public:/app/public:ro
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
- dek-vol:/run/secrets
|
- dek-vol:/run/secrets:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER2_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -73,8 +96,18 @@ services:
|
|||||||
server3:
|
server3:
|
||||||
<<: *with-env
|
<<: *with-env
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG}
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${IMG_TAG}
|
||||||
|
user: "1000:1000"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
expose: ["${SERVER3_PORT}"]
|
expose: ["${SERVER3_PORT}"]
|
||||||
environment:
|
environment:
|
||||||
|
ENV_NAME: ${ENV_NAME}
|
||||||
|
PROJECT: ${PROJECT}
|
||||||
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
KMS_KEY_NAME: ${KMS_KEY_NAME}
|
||||||
DEK_PATH: ${DEK_PATH}
|
DEK_PATH: ${DEK_PATH}
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
@ -103,7 +136,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./salary_info.db:/app/salary_info.db:ro
|
- ./salary_info.db:/app/salary_info.db:ro
|
||||||
- ./user_profile.db:/app/user_profile.db
|
- ./user_profile.db:/app/user_profile.db
|
||||||
- dek-vol:/run/secrets
|
- dek-vol:/run/secrets:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:${SERVER3_PORT}/healthz || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -113,7 +146,7 @@ services:
|
|||||||
# ───────────────────────────── nginx ───────────────────────────────
|
# ───────────────────────────── nginx ───────────────────────────────
|
||||||
nginx:
|
nginx:
|
||||||
<<: *with-env
|
<<: *with-env
|
||||||
image: nginx:1.25-alpine
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/nginx:${IMG_TAG}
|
||||||
command: ["nginx", "-g", "daemon off;"]
|
command: ["nginx", "-g", "daemon off;"]
|
||||||
depends_on: [server1, server2, server3]
|
depends_on: [server1, server2, server3]
|
||||||
networks: [default, aptiva-shared]
|
networks: [default, aptiva-shared]
|
||||||
@ -131,6 +164,6 @@ networks:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dek-vol:
|
dek-vol:
|
||||||
name:
|
name: aptiva_dek_dev
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
12
docker-entrypoint.sh
Normal file
12
docker-entrypoint.sh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 1. Derive the directory from DEK_PATH (e.g. /run/secrets/dev/dek.enc ⇒ /run/secrets/dev)
|
||||||
|
EDEK_DIR="$(dirname "${DEK_PATH}")"
|
||||||
|
|
||||||
|
# 2. Make sure it exists and is owned by UID 1000 (the “node” user in the official image)
|
||||||
|
mkdir -p "${EDEK_DIR}"
|
||||||
|
chown -R 1000:1000 "${EDEK_DIR}"
|
||||||
|
|
||||||
|
# 3. Chain‑exec as the unprivileged user
|
||||||
|
exec gosu node "$@"
|
@ -36,6 +36,8 @@ ALTER TABLE career_profiles
|
|||||||
MODIFY career_goals MEDIUMTEXT,
|
MODIFY career_goals MEDIUMTEXT,
|
||||||
MODIFY desired_retirement_income_monthly VARCHAR(128);
|
MODIFY desired_retirement_income_monthly VARCHAR(128);
|
||||||
|
|
||||||
|
encryption_canary(id INTEGER PRIMARY KEY, value TEXT)
|
||||||
|
|
||||||
/* ────────────────────────────────────────────────────────────────
|
/* ────────────────────────────────────────────────────────────────
|
||||||
college_profiles – migrate for encrypted VARCHAR columns
|
college_profiles – migrate for encrypted VARCHAR columns
|
||||||
────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────
|
||||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/kms": "^5.1.0",
|
"@google-cloud/kms": "^5.1.0",
|
||||||
|
"@google-cloud/secret-manager": "^6.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
@ -2511,6 +2512,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@google-cloud/secret-manager": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-IrXjT1z2yW98htydkopcxdhNVh4rpAzO3oTVzKymfdnzmzCKWVxhfwYWWvIor1bzgQN4sa21Wv0CMDokJyTt7A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"google-gax": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@grpc/grpc-js": {
|
"node_modules/@grpc/grpc-js": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/kms": "^5.1.0",
|
"@google-cloud/kms": "^5.1.0",
|
||||||
|
"@google-cloud/secret-manager": "^6.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-progress": "^1.1.2",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user