From d6ac72e6cf19ffca3957a5d1aacda9063d407d38 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Feb 2026 20:06:42 -0800 Subject: [PATCH] feat: wire rmaps sync server to pull secrets from Infisical at startup Co-Authored-By: Claude Opus 4.6 --- sync-server/Dockerfile | 5 +++ sync-server/docker-compose.yml | 10 ++++- sync-server/entrypoint.sh | 82 ++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100755 sync-server/entrypoint.sh diff --git a/sync-server/Dockerfile b/sync-server/Dockerfile index 921c5dd..7eee304 100644 --- a/sync-server/Dockerfile +++ b/sync-server/Dockerfile @@ -15,6 +15,10 @@ RUN npm ci --only=production # Copy server code and fix ownership COPY --chown=nodejs:nodejs server.js verify-token.js ./ +# Copy and set up Infisical entrypoint +COPY --chown=nodejs:nodejs entrypoint.sh ./ +RUN chmod +x entrypoint.sh + # Create data directory for persistence and set ownership RUN mkdir -p /app/data && chown -R nodejs:nodejs /app @@ -22,4 +26,5 @@ USER nodejs EXPOSE 3001 +ENTRYPOINT ["/app/entrypoint.sh"] CMD ["node", "server.js"] diff --git a/sync-server/docker-compose.yml b/sync-server/docker-compose.yml index 93b0e88..f369780 100644 --- a/sync-server/docker-compose.yml +++ b/sync-server/docker-compose.yml @@ -5,9 +5,15 @@ services: restart: unless-stopped environment: - PORT=3001 - # VAPID keys for push notifications + # Infisical secret injection + - INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID} + - INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET} + - INFISICAL_PROJECT_SLUG=rmaps + - INFISICAL_ENV=prod + - INFISICAL_URL=http://infisical:8080 + # VAPID keys for push notifications (injected by Infisical at runtime) - VAPID_PUBLIC_KEY=BNWACJudUOeHEZKEFB-0Wz086nHYsWzj12LqQ7lsUNT38ThtNUoZTJYEH9lttQitCROE2G3Ob71ZUww47yvCDbk - - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY:-x3yCse1Q4rbZ1XLgnJ1KpSuRlw2ccHDW0fMcKtQ1qcw} + - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} - VAPID_SUBJECT=mailto:push@rmaps.online # Automatic location request interval (ms) - 0 to disable - LOCATION_REQUEST_INTERVAL=60000 diff --git a/sync-server/entrypoint.sh b/sync-server/entrypoint.sh new file mode 100755 index 0000000..04bdfaa --- /dev/null +++ b/sync-server/entrypoint.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Infisical secret injection entrypoint +# Fetches secrets from Infisical API and injects them as env vars before starting the app. +# Required env vars: INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET +# Optional: INFISICAL_PROJECT_SLUG (default: rmaps), INFISICAL_ENV (default: prod), +# INFISICAL_URL (default: http://infisical:8080) + +set -e + +INFISICAL_URL="${INFISICAL_URL:-http://infisical:8080}" +INFISICAL_ENV="${INFISICAL_ENV:-prod}" +INFISICAL_PROJECT_SLUG="${INFISICAL_PROJECT_SLUG:-rmaps}" + +if [ -z "$INFISICAL_CLIENT_ID" ] || [ -z "$INFISICAL_CLIENT_SECRET" ]; then + echo "[infisical] No credentials set, starting without secret injection" + exec "$@" +fi + +echo "[infisical] Fetching secrets from ${INFISICAL_PROJECT_SLUG}/${INFISICAL_ENV}..." + +# Use Node.js (already in the image) for reliable JSON parsing and HTTP calls +EXPORTS=$(node -e " +const http = require('http'); +const https = require('https'); +const url = new URL(process.env.INFISICAL_URL); +const client = url.protocol === 'https:' ? https : http; + +const post = (path, body) => new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const req = client.request({ hostname: url.hostname, port: url.port, path, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } + }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d))); }); + req.on('error', reject); + req.end(data); +}); + +const get = (path, token) => new Promise((resolve, reject) => { + const req = client.request({ hostname: url.hostname, port: url.port, path, method: 'GET', + headers: { 'Authorization': 'Bearer ' + token } + }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d))); }); + req.on('error', reject); + req.end(); +}); + +(async () => { + try { + const auth = await post('/api/v1/auth/universal-auth/login', { + clientId: process.env.INFISICAL_CLIENT_ID, + clientSecret: process.env.INFISICAL_CLIENT_SECRET + }); + if (!auth.accessToken) { console.error('[infisical] Auth failed'); process.exit(1); } + + const slug = process.env.INFISICAL_PROJECT_SLUG; + const env = process.env.INFISICAL_ENV; + const secrets = await get('/api/v3/secrets/raw?workspaceSlug=' + slug + '&environment=' + env + '&secretPath=/&recursive=true', auth.accessToken); + + if (!secrets.secrets) { console.error('[infisical] No secrets returned'); process.exit(1); } + + // Output as shell-safe export statements + for (const s of secrets.secrets) { + // Single-quote the value to prevent shell expansion, escape existing single quotes + const escaped = s.secretValue.replace(/'/g, \"'\\\\''\" ); + console.log('export ' + s.secretKey + \"='\" + escaped + \"'\"); + } + } catch (e) { console.error('[infisical] Error:', e.message); process.exit(1); } +})(); +" 2>&1) || { + echo "[infisical] WARNING: Failed to fetch secrets, starting with existing env vars" + exec "$@" +} + +# Check if we got export statements or error messages +if echo "$EXPORTS" | grep -q "^export "; then + COUNT=$(echo "$EXPORTS" | grep -c "^export ") + eval "$EXPORTS" + echo "[infisical] Injected ${COUNT} secrets" +else + echo "[infisical] WARNING: $EXPORTS" + echo "[infisical] Starting with existing env vars" +fi + +exec "$@"