From f3327d29d4d35479a6566d9f2ff8f2379556d0a6 Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Sun, 24 May 2026 09:18:06 -0500 Subject: [PATCH] Update to use a base64 encoded key --- .env.example | 61 ++++++++++++--------- Dockerfile | 129 ++++++++++++++++++++++++++------------------- docker-compose.yml | 47 +++++++++-------- 3 files changed, 136 insertions(+), 101 deletions(-) diff --git a/.env.example b/.env.example index ec3683e..4c6c0d2 100644 --- a/.env.example +++ b/.env.example @@ -37,34 +37,45 @@ NODE_ENV=development # ────────────────────────────────────────────────────────────────── # Build-time — values the `docker compose build` step uses # ────────────────────────────────────────────────────────────────── -# These are only needed when BUILDING the image, not when running -# it. The image talks to git.kadil.dev to fetch the `goods-editor` -# git+ssh: npm dependency during `npm install`. See the docblock -# in Dockerfile for the full mechanism. +# Only needed when BUILDING the image, not when running it. The +# image talks to git.kadil.dev to fetch the `goods-editor` git+ssh: +# npm dependency during `npm install`. See the Dockerfile docblock +# for the full mechanism. # -# ── Option A: file-based key (typical local dev / single deploy host) -# Set this to the absolute path of a private SSH key that has read -# access to the goods-editor-module repo. The key file stays on -# disk; only its contents are mounted into the build for the one -# RUN step that needs it, and nothing about it ends up in the -# image layers. +# The Dockerfile expects a BASE64-ENCODED private key (not raw PEM) +# because that's the format Dokploy's Build-time Secrets textarea +# requires — it's dotenv-parsed and can't handle multi-line values. +# We use base64 for local builds too so the Dockerfile has one input +# format. Encode like this: # -# Recommended: generate a dedicated read-only deploy key rather -# than reusing a personal key (see Dockerfile docblock for the -# ssh-keygen + Gitea deploy-key registration steps). +# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > /tmp/key.b64 # -# Examples (uncomment one): -# SSH_KEY_FILE=/Users/khalid/.ssh/goods-editor-deploy -# SSH_KEY_FILE=/home/deploy/.ssh/goods-editor-deploy +# Recommended: generate a dedicated read-only deploy key rather than +# reusing a personal key (see Dockerfile docblock for the ssh-keygen +# + Gitea deploy-key registration steps). +# +# Path to a base64-encoded key file. Uncomment one for local builds: +# SSH_KEY_FILE=/Users/khalid/.ssh/goods-editor-deploy.b64 +# SSH_KEY_FILE=/home/deploy/.ssh/goods-editor-deploy.b64 -# ── Option B: env-var-based key (typical CI) -# Pass the key contents directly via SSH_PRIVATE_KEY. Don't put -# the actual key in this .env file (multi-line values are fragile -# in env files, and committing a key — even .env.example with -# real material — is a leak waiting to happen). Set it from the -# shell or from your CI's secret store: +# Alternative: env-var-based (CI). To use this mode, also flip +# docker-compose.yml's `secrets.ssh_key` block from `file:` to +# `environment:` (see comments there). Set the base64 contents via +# the shell or your CI secret store: # -# SSH_PRIVATE_KEY="$(cat ~/.ssh/goods-editor-deploy)" docker compose build +# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \ +# docker compose build # -# To use this mode, also flip docker-compose.yml's `secrets.ssh_key` -# block from `file:` to `environment:` (see comments there). +# Don't put the actual base64 in this .env file. The string is long +# enough that dotenv parsers may struggle, and committing real key +# material — even base64 — to a repo is a leak waiting to happen. +# +# ── Dokploy ─────────────────────────────────────────────────────── +# The entries above DON'T apply to Dokploy. In Dokploy, paste the +# base64 directly into the application's Environment → Build-time +# Secrets textarea as a single line: +# +# ssh_key= +# +# Dokploy parses that textarea as dotenv and passes the value into +# the build as a BuildKit secret named `ssh_key`. diff --git a/Dockerfile b/Dockerfile index 4d0787e..7eee273 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,35 +4,47 @@ # ──────────── # This Dockerfile needs SSH access to git.kadil.dev during the # `npm install` step (the `goods-editor` dependency is a git+ssh: -# URL). We pass an SSH private key in as a BuildKit secret named -# `ssh_key`. The secret is mounted into the build for just that one -# RUN step, used to authenticate the git fetch, then unmounted — -# nothing about the key ends up in any image layer or in -# `docker history`. +# URL). We pass a base64-encoded SSH private key as a BuildKit +# secret named `ssh_key`, then decode it inside the build. # -# Get an SSH key with read access to the goods-editor-module repo -# on Gitea registered first (the same key you use for git push works, -# OR a dedicated deploy key with read-only access — recommended for -# CI / servers). The public half goes in Gitea Settings → SSH Keys, -# or as a deploy key on the repo itself. +# Why base64-encoded rather than raw PEM +# ─────────────────────────────────────── +# The primary deploy target is Dokploy, whose Build-time Secrets UI +# is a dotenv-parsed textarea (one KEY=VALUE per line). PEM keys are +# multi-line and break dotenv parsing. Base64 collapses the key to a +# single line of safe ASCII that any dotenv parser accepts. We use +# base64 everywhere (Dokploy and local builds) so the Dockerfile has +# exactly one input format — no branching logic for raw vs encoded. # -# Then build, picking whichever invocation matches where the key is: +# Register a deploy key first +# ───────────────────────────── +# Generate a read-only deploy key dedicated to this build and add +# its public half to Gitea (repo Settings → Deploy Keys): +# ssh-keygen -t ed25519 -f ~/.ssh/goods-editor-deploy -N "" -C "deploy" +# cat ~/.ssh/goods-editor-deploy.pub # → paste into Gitea # -# 1. From a file on disk (typical local dev / deployment server): -# docker build \ -# --secret id=ssh_key,src=$HOME/.ssh/id_ed25519 \ -# -t apparel-designer . +# Then base64-encode the PRIVATE half for build-time use: +# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy +# The `tr -d '\n'` strips line wrapping so the output is a single +# physical line, which the Dokploy textarea (and dotenv parsers in +# general) require. # -# 2. From an environment variable (typical CI — secret store -# injects the key as an env var, no file ever touches disk): -# docker build \ -# --secret id=ssh_key,env=SSH_PRIVATE_KEY \ -# -t apparel-designer . -# (BuildKit 1.5+ supports the `env=` source.) +# Dokploy +# ─────── +# Paste into the application's Environment → Build-time Secrets +# textarea as a single line: +# ssh_key= +# Then redeploy. # -# 3. Via docker-compose: see docker-compose.yml — the `secrets:` -# block there wires the same secret. Then: -# SSH_KEY_FILE=$HOME/.ssh/id_ed25519 docker compose build +# Local build with the same approach +# ─────────────────────────────────── +# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > /tmp/key.b64 +# docker build --secret id=ssh_key,src=/tmp/key.b64 -t apparel-designer . +# rm /tmp/key.b64 +# +# Or via env var: +# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \ +# docker build --secret id=ssh_key,env=SSH_KEY_B64 -t apparel-designer . # # Requires Docker 23+ (BuildKit is the default; older Docker needs # `DOCKER_BUILDKIT=1` exported in the environment). @@ -97,42 +109,53 @@ COPY package*.json ./ # a host-provided secret at /run/secrets/ssh_key inside this single # RUN step. The secret is NEVER copied into any image layer; once # the RUN command finishes, the mount disappears and nothing about -# the key remains in the built image. `docker history` won't show -# it either. +# the key remains in the built image. # -# Steps: -# 1. Create ~/.ssh with 0700 perms (ssh refuses to use a config -# directory that's world-readable). -# 2. ssh-keyscan git.kadil.dev → ~/.ssh/known_hosts. Without this, -# ssh would prompt "Are you sure you want to continue connecting?" -# with no stdin to answer it, and the install would hang forever. -# 3. Run npm install with GIT_SSH_COMMAND pointing at the mounted -# secret. npm shells out to git for the git+ssh: dependency, git -# respects GIT_SSH_COMMAND, ssh uses the key at /run/secrets/ssh_key. -# `IdentitiesOnly=yes` prevents ssh from trying any other key -# it might find (none in this container, but defensive). +# Dokploy-specific note: Dokploy's Build-time Secrets UI is a single +# textarea that gets parsed as dotenv (one KEY=VALUE per line), +# which doesn't tolerate multi-line values. PEM-formatted SSH keys +# ARE multi-line. The workaround is to base64-encode the key before +# pasting into Dokploy and decode here at use-time. Base64 produces +# a single line of ASCII with no characters that confuse dotenv +# parsers. # -# Why use GIT_SSH_COMMAND rather than copying the key to ~/.ssh/id_ed25519: -# the secret mount is a clean way to use a file without it ever -# touching the filesystem proper. If we copied, we'd have to be -# careful to `rm` it in the same RUN step before layer commit; with -# GIT_SSH_COMMAND, the file is only ever at /run/secrets/, which -# BuildKit clears automatically. +# Encode locally: +# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' | pbcopy +# Then paste in Dokploy as: +# ssh_key= +# +# Steps in this RUN: +# 1. mkdir ~/.ssh with 0700 (ssh refuses world-readable config dirs) +# 2. ssh-keyscan git.kadil.dev so we don't hang on the +# "continue connecting?" prompt +# 3. base64-decode /run/secrets/ssh_key into /tmp/ssh_key. The +# decoded file holds the original PEM private key. +# 4. chmod 0600 the decoded key so ssh accepts it. +# 5. Run npm install with GIT_SSH_COMMAND pointing at the decoded +# key. IdentitiesOnly=yes prevents ssh from trying any other +# key it might find. +# 6. rm the decoded key. Since we created it in the same RUN, it +# never gets committed to a layer. # # If the build aborts here: -# • "Permission denied (publickey)" → the secret was provided but -# the key doesn't have access to the repo. Verify the public -# half is registered in Gitea (account settings or deploy key). -# • "required secret 'ssh_key' not provided" → forgot the `--secret` -# flag on the build command (see docblock at top of file). -# • Hangs at "Cloning into …" → ssh-keyscan didn't populate -# known_hosts (network issue inside the build). Try again with -# `--no-cache`. +# • "Permission denied (publickey)" → the key in Dokploy doesn't +# match the deploy key registered in Gitea. Re-base64-encode and +# re-paste, OR re-check the Gitea deploy key. +# • "Identity file /tmp/ssh_key not accessible: No such file or +# directory" → the secret wasn't mounted. Verify the Dokploy +# Build-time Secrets textarea contains exactly `ssh_key=...` as +# a single line. +# • "base64: invalid input" → the Dokploy textarea has trailing +# whitespace, line breaks inside the base64, or extra content. +# Re-paste from a fresh `base64 < key | tr -d '\n' | pbcopy`. RUN --mount=type=secret,id=ssh_key \ mkdir -p -m 0700 ~/.ssh && \ ssh-keyscan git.kadil.dev >> ~/.ssh/known_hosts 2>/dev/null && \ - GIT_SSH_COMMAND="ssh -i /run/secrets/ssh_key -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" \ - npm install + base64 -d /run/secrets/ssh_key > /tmp/ssh_key && \ + chmod 0600 /tmp/ssh_key && \ + GIT_SSH_COMMAND="ssh -i /tmp/ssh_key -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts" \ + npm install && \ + rm -f /tmp/ssh_key COPY . . diff --git a/docker-compose.yml b/docker-compose.yml index a130586..1e63885 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,14 @@ services: build: context: . dockerfile: Dockerfile - # Pass an SSH private key in as a build secret so npm can - # authenticate against git.kadil.dev for the `goods-editor` - # git+ssh: dependency. The secret is only available during - # the single RUN step that mounts it; nothing about the key - # ends up in the built image. See Dockerfile docblock for - # the full mechanism. + # Pass a base64-encoded SSH private key in as a build secret so + # npm can authenticate against git.kadil.dev for the `goods-editor` + # git+ssh: dependency. The Dockerfile decodes the base64 inside + # the build. See Dockerfile docblock for the full mechanism and + # the rationale for base64 over raw PEM. + # + # The secret is only available during the single RUN step that + # mounts it; nothing about the key ends up in the built image. secrets: - ssh_key container_name: apparel-designer @@ -23,33 +25,32 @@ services: # Build-time secret declaration. # -# Two ways to provide the key, pick whichever fits where you're -# building from: +# The Dockerfile expects a BASE64-ENCODED private key (not the raw +# PEM). See the Dockerfile docblock for the encoding command. Two +# ways to provide it, pick whichever fits where you're building from: # -# ── A. File-based (typical local dev / deployment server) ─────── -# Set SSH_KEY_FILE in the environment before running compose: +# ── A. File-based (typical local dev) ────────────────────────────── +# Encode the key to a temp file once, then point SSH_KEY_FILE at it: # -# SSH_KEY_FILE=$HOME/.ssh/id_ed25519 docker compose build +# base64 < ~/.ssh/goods-editor-deploy | tr -d '\n' > ~/.ssh/goods-editor-deploy.b64 +# SSH_KEY_FILE=$HOME/.ssh/goods-editor-deploy.b64 docker compose build # -# The `file:` source below reads from that path. The fallback -# `/dev/null` exists so `docker compose config` (validation / -# linting) doesn't error out when the env var isn't set — actual -# builds against /dev/null will fail with a clear "permission -# denied" rather than a cryptic compose error. +# The fallback `/dev/null` keeps `docker compose config` from erroring +# when SSH_KEY_FILE isn't set; actual builds against /dev/null fail +# with a clear base64-decode error rather than a cryptic compose error. # -# ── B. Env-var-based (typical CI) ────────────────────────────── +# ── B. Env-var-based (CI) ────────────────────────────────────────── # Comment out the `file:` line and uncomment `environment:` below. -# Provide the key contents in the SSH_PRIVATE_KEY env var: +# Provide the base64-encoded key in SSH_KEY_B64: # -# SSH_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" docker compose build +# SSH_KEY_B64="$(base64 < ~/.ssh/goods-editor-deploy | tr -d '\n')" \ +# docker compose build # -# In GitHub Actions / GitLab CI / etc., set SSH_PRIVATE_KEY from -# the platform's secret store. The `environment:` source for build -# secrets requires Docker Compose v2.23+. +# Requires Docker Compose v2.23+ for the `environment:` secret source. secrets: ssh_key: file: ${SSH_KEY_FILE:-/dev/null} - # environment: SSH_PRIVATE_KEY + # environment: SSH_KEY_B64 volumes: uploads_data: