feat: wire Postiz spaces to pull secrets from Infisical at runtime

- Template uses entrypoint-wrapper.sh to inject secrets at container start
- Only INFISICAL_* credentials + POSTGRES_PASSWORD in .env (3 values)
- All other secrets (JWT, EMAIL_PASS, OAuth, social API keys) from Infisical
- Generator produces minimal .env templates per space
- Added infisical_slug to spaces.yml for each Postiz project
- Added missing EMAIL_PASS + POSTGRES_PASSWORD to Infisical projects

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-24 21:10:59 -08:00
parent dc95494ab8
commit 8ef5c678c2
4 changed files with 54 additions and 35 deletions

View File

@ -1,9 +1,10 @@
---
id: TASK-6
title: Remove plaintext .env files from server
status: To Do
status: In Progress
assignee: []
created_date: '2026-02-25 05:02'
updated_date: '2026-02-25 05:04'
labels:
- security
- infisical

View File

@ -4,17 +4,31 @@
# Generated by generate.sh from spaces.yml — DO NOT EDIT DIRECTLY.
# To modify, edit spaces.yml and re-run: ./generate.sh
# Primary: {{PRIMARY_DOMAIN}} | Fallback: {{FALLBACK_DOMAIN}}
#
# Secrets (JWT_SECRET, EMAIL_PASS, social API keys, OAuth secrets) are
# injected at runtime by the Infisical entrypoint wrapper.
# Only INFISICAL_* credentials and POSTGRES_PASSWORD go in .env.
services:
postiz-{{SPACE_SLUG}}:
image: {{POSTIZ_IMAGE}}
container_name: postiz-{{SPACE_SLUG}}
restart: unless-stopped
# Infisical wrapper injects secrets then runs original entrypoint
entrypoint: ["/infisical-entrypoint.sh"]
command: ["docker-entrypoint.sh", "sh", "-c", "nginx && pnpm run pm2"]
environment:
# --- Infisical (credentials from .env) ---
INFISICAL_CLIENT_ID: '${INFISICAL_CLIENT_ID}'
INFISICAL_CLIENT_SECRET: '${INFISICAL_CLIENT_SECRET}'
INFISICAL_PROJECT_SLUG: '{{INFISICAL_SLUG}}'
INFISICAL_ENV: 'prod'
INFISICAL_URL: 'http://infisical:8080'
# --- App Config (non-secret, stays in compose) ---
MAIN_URL: 'https://{{PRIMARY_DOMAIN}}'
FRONTEND_URL: 'https://{{PRIMARY_DOMAIN}}'
NEXT_PUBLIC_BACKEND_URL: 'https://{{PRIMARY_DOMAIN}}/api'
JWT_SECRET: '${JWT_SECRET}'
DATABASE_URL: 'postgresql://postiz:${POSTGRES_PASSWORD}@postiz-{{SPACE_SLUG}}-postgres:5432/postiz'
REDIS_URL: 'redis://postiz-{{SPACE_SLUG}}-redis:6379'
BACKEND_INTERNAL_URL: 'http://localhost:3000'
@ -22,7 +36,7 @@ services:
IS_GENERAL: '{{IS_GENERAL}}'
DISABLE_REGISTRATION: '{{DISABLE_REG}}'
# Pocket ID OAuth
# Pocket ID OAuth (config only — client_id/secret from Infisical)
{{OAUTH_BLOCK}}
# Storage
@ -30,29 +44,7 @@ services:
UPLOAD_DIRECTORY: '{{UPLOAD_DIR}}'
NEXT_PUBLIC_UPLOAD_DIRECTORY: '{{UPLOAD_DIR}}'
# Social Media API Settings (from .env or Infisical)
X_API_KEY: '${X_API_KEY:-}'
X_API_SECRET: '${X_API_SECRET:-}'
LINKEDIN_CLIENT_ID: '${LINKEDIN_CLIENT_ID:-}'
LINKEDIN_CLIENT_SECRET: '${LINKEDIN_CLIENT_SECRET:-}'
REDDIT_CLIENT_ID: '${REDDIT_CLIENT_ID:-}'
REDDIT_CLIENT_SECRET: '${REDDIT_CLIENT_SECRET:-}'
THREADS_APP_ID: '${THREADS_APP_ID:-}'
THREADS_APP_SECRET: '${THREADS_APP_SECRET:-}'
FACEBOOK_APP_ID: '${FACEBOOK_APP_ID:-}'
FACEBOOK_APP_SECRET: '${FACEBOOK_APP_SECRET:-}'
YOUTUBE_CLIENT_ID: '${YOUTUBE_CLIENT_ID:-}'
YOUTUBE_CLIENT_SECRET: '${YOUTUBE_CLIENT_SECRET:-}'
TIKTOK_CLIENT_ID: '${TIKTOK_CLIENT_ID:-}'
TIKTOK_CLIENT_SECRET: '${TIKTOK_CLIENT_SECRET:-}'
DISCORD_CLIENT_ID: '${DISCORD_CLIENT_ID:-}'
DISCORD_CLIENT_SECRET: '${DISCORD_CLIENT_SECRET:-}'
DISCORD_BOT_TOKEN_ID: '${DISCORD_BOT_TOKEN_ID:-}'
MASTODON_URL: '${MASTODON_URL:-https://mastodon.social}'
MASTODON_CLIENT_ID: '${MASTODON_CLIENT_ID:-}'
MASTODON_CLIENT_SECRET: '${MASTODON_CLIENT_SECRET:-}'
# Email
# Email (config only — EMAIL_PASS from Infisical)
EMAIL_PROVIDER: '{{EMAIL_PROVIDER}}'
EMAIL_FROM_NAME: '{{EMAIL_FROM_NAME}}'
EMAIL_FROM_ADDRESS: '{{EMAIL_FROM}}'
@ -60,17 +52,19 @@ services:
EMAIL_PORT: '{{EMAIL_PORT}}'
EMAIL_SECURE: '{{EMAIL_SECURE}}'
EMAIL_USER: '{{EMAIL_USER}}'
EMAIL_PASS: '${EMAIL_PASS}'
NODE_TLS_REJECT_UNAUTHORIZED: '0'
# AI
OPENAI_API_KEY: '${OPENAI_API_KEY:-}'
# Misc
NX_ADD_PLUGINS: false
API_LIMIT: {{API_LIMIT}}
# --- Secrets injected by Infisical at runtime ---
# JWT_SECRET, EMAIL_PASS, OPENAI_API_KEY,
# POSTIZ_OAUTH_CLIENT_ID, POSTIZ_OAUTH_CLIENT_SECRET,
# X_API_KEY, X_API_SECRET, LINKEDIN_CLIENT_ID, etc.
volumes:
- /opt/infisical/entrypoint-wrapper.sh:/infisical-entrypoint.sh:ro
- postiz-{{SPACE_SLUG}}-config:/config/
- postiz-{{SPACE_SLUG}}-uploads:/uploads/
labels:

View File

@ -79,11 +79,12 @@ get_space_override() {
generate_space() {
local space="$1"
local primary_domain fallback_domain email_from slug
local primary_domain fallback_domain email_from slug infisical_slug
primary_domain=$(get_space "$space" "primary_domain")
fallback_domain=$(get_space "$space" "fallback_domain")
email_from=$(get_space "$space" "email_from")
slug=$(get_space "$space" "slug")
infisical_slug=$(get_space "$space" "infisical_slug")
if [[ -z "$primary_domain" || "$primary_domain" == "null" ]]; then
echo -e "${RED} Error: spaces.${space}.primary_domain is required${NC}"
@ -95,6 +96,11 @@ generate_space() {
slug="$space"
fi
# Infisical slug defaults to postiz-<space>
if [[ -z "$infisical_slug" || "$infisical_slug" == "null" ]]; then
infisical_slug="postiz-${space}"
fi
# Read defaults (with per-space overrides)
local postiz_image postiz_port postgres_image redis_image temporal_image temporal_pg_image
local email_provider email_from_name email_host email_port email_secure email_user
@ -141,7 +147,7 @@ generate_space() {
# Build dynamic blocks
# -----------------------------------------------------------------------
# OAuth block
# OAuth block (config only — client_id/secret injected by Infisical)
local oauth_block
if [[ "$use_oauth" == "true" ]]; then
oauth_block=" POSTIZ_GENERIC_OAUTH: 'true'
@ -151,9 +157,8 @@ generate_space() {
POSTIZ_OAUTH_AUTH_URL: '${oauth_auth_url}'
POSTIZ_OAUTH_TOKEN_URL: '${oauth_token_url}'
POSTIZ_OAUTH_USERINFO_URL: '${oauth_userinfo_url}'
POSTIZ_OAUTH_CLIENT_ID: '\${POSTIZ_OAUTH_CLIENT_ID}'
POSTIZ_OAUTH_CLIENT_SECRET: '\${POSTIZ_OAUTH_CLIENT_SECRET}'
POSTIZ_OAUTH_SCOPE: 'openid profile email'"
POSTIZ_OAUTH_SCOPE: 'openid profile email'
# POSTIZ_OAUTH_CLIENT_ID + CLIENT_SECRET from Infisical"
else
oauth_block=" # OAuth disabled for this space"
fi
@ -209,6 +214,7 @@ generate_space() {
-e "s|{{DISABLE_REG}}|${disable_reg}|g" \
-e "s|{{IS_GENERAL}}|${is_general}|g" \
-e "s|{{API_LIMIT}}|${api_limit}|g" \
-e "s|{{INFISICAL_SLUG}}|${infisical_slug}|g" \
"$TEMPLATE" > "$outfile"
# Replace multi-line blocks (sed can't do these well, use temp files)
@ -235,6 +241,21 @@ generate_space() {
echo -e "${GREEN}${outfile}${NC}"
# Generate minimal .env template (only Infisical creds + postgres password)
local envfile="${OUTDIR}/env.space-${space}"
cat > "$envfile" <<EOF
# Minimal secrets for ${space} — all other secrets from Infisical
# Copy to server alongside docker-compose.space-${space}.yml
# Infisical Machine Identity (shared across spaces)
INFISICAL_CLIENT_ID=
INFISICAL_CLIENT_SECRET=
# Postgres password (needed by postgres container directly)
POSTGRES_PASSWORD=
EOF
echo -e "${GREEN}${envfile}${NC}"
# Collect domains for tunnel/dns generation
ALL_PRIMARY_DOMAINS+=("$primary_domain")
ALL_FALLBACK_DOMAINS+=("$fallback_domain")

View File

@ -67,6 +67,7 @@ spaces:
fallback_domain: socials.valleyofthecommons.com
email_from: noreply@rmail.online
infisical_project_id: a76d3d2c-205e-4356-a8ad-2a2c19724d8c
infisical_slug: postiz-crypto-commons
postiz:
disable_registration: true
email_from_name: Crypto Commons
@ -79,6 +80,7 @@ spaces:
fallback_domain: socials.p2pfoundation.net
email_from: noreply@rmail.online
infisical_project_id: ea4b3b47-12d5-40d1-9a80-30bc48cdba7a
infisical_slug: postiz-p2pfoundation
postiz:
email_from_name: P2P Foundation Socials
services:
@ -90,6 +92,7 @@ spaces:
fallback_domain: socials.bondingcurve.tech
email_from: noreply@rmail.online
infisical_project_id: 255cb27d-e8d3-459d-af2c-faff55b46d9f
infisical_slug: postiz-bondingcurve
postiz:
email_from_name: Bonding Curve Research
services: