From 07c149b0c80452b22461ea4c5fe779d9209cdc3b Mon Sep 17 00:00:00 2001 From: Francesc Leveque Date: Sun, 24 May 2026 10:48:44 +0200 Subject: [PATCH 1/3] Snapshot current 1Password secrets file as .example Preserve the working 1Password setup so future-you can swap back with a single cp. Verbatim copy of .kamal/secrets as of the last 1P-driven deploy. Co-Authored-By: Claude Opus 4.7 (1M context) --- .kamal/secrets.1password.example | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .kamal/secrets.1password.example diff --git a/.kamal/secrets.1password.example b/.kamal/secrets.1password.example new file mode 100644 index 0000000..483e93d --- /dev/null +++ b/.kamal/secrets.1password.example @@ -0,0 +1,22 @@ +# Secrets for Kamal deployment — sourced from 1Password. +# +# Requires OP_SERVICE_ACCOUNT_TOKEN, OP_ACCOUNT, OP_VAULT in the shell env +# (local: repo .env via direnv; CI: deploy.yml env block). +# +# Parsed by dotenv line-by-line (NOT bash): each $(...) must stay on one line, +# and only ${VAR} interpolation is supported (no ${VAR:-default}). + +REGISTRY=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/registry KAMAL_REGISTRY_PASSWORD) +LOGO=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/logo-service LOGO_AUTH_API_KEYS LOGO_AUTH_ADMIN_KEYS) +# Reuse the dividend-portfolio Gemini key — both apps share Google's project-level quota anyway. +DP=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/dividend-portfolio GEMINI_API_KEY) + +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) +LOGO_AUTH_API_KEYS=$(kamal secrets extract LOGO_AUTH_API_KEYS ${LOGO}) +LOGO_AUTH_ADMIN_KEYS=$(kamal secrets extract LOGO_AUTH_ADMIN_KEYS ${LOGO}) +LOGO_LLM_GEMINI_API_KEY=$(kamal secrets extract GEMINI_API_KEY ${DP}) + +# Anthropic and OpenAI keys stay optional fallbacks — export them in the deploy +# shell env to enable, otherwise the chain just skips them. +LOGO_LLM_ANTHROPIC_API_KEY=$LOGO_LLM_ANTHROPIC_API_KEY +LOGO_LLM_OPENAI_API_KEY=$LOGO_LLM_OPENAI_API_KEY From cf6779484fe988f2fd6b83e65f167f14ecba075f Mon Sep 17 00:00:00 2001 From: Francesc Leveque Date: Sun, 24 May 2026 22:04:16 +0200 Subject: [PATCH 2/3] Switch .kamal/secrets to bitwarden Same set of secret keys; only the storage backend changes. All four secrets live in a shared Bitwarden item named `quantic-prod`, with each secret as a custom field on the item. GEMINI_API_KEY is still extracted into LOGO_LLM_GEMINI_API_KEY so the app-level env name stays the same. Why consumer Bitwarden over Bitwarden Secrets Manager (BWS): BWS Free caps at 3 projects per org, which would bite us as more services land. Consumer has no item cap on the free tier. This commit alone breaks deploy until the workflow change in the next commit, so don't deploy from this state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .kamal/secrets | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.kamal/secrets b/.kamal/secrets index 483e93d..0953781 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -1,20 +1,21 @@ -# Secrets for Kamal deployment — sourced from 1Password. +# Secrets for Kamal deployment — sourced from Bitwarden Password Manager. # -# Requires OP_SERVICE_ACCOUNT_TOKEN, OP_ACCOUNT, OP_VAULT in the shell env -# (local: repo .env via direnv; CI: deploy.yml env block). +# Requires BW_CLIENTID, BW_CLIENTSECRET, and BW_PASSWORD in the shell env +# (local: repo .env via direnv; CI: deploy.yml env block). The `bw` CLI must +# be on PATH; kamal's bitwarden adapter handles login + vault unlock. # # Parsed by dotenv line-by-line (NOT bash): each $(...) must stay on one line, # and only ${VAR} interpolation is supported (no ${VAR:-default}). +# +# To switch back to 1Password: see .kamal/secrets.1password.example. -REGISTRY=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/registry KAMAL_REGISTRY_PASSWORD) -LOGO=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/logo-service LOGO_AUTH_API_KEYS LOGO_AUTH_ADMIN_KEYS) -# Reuse the dividend-portfolio Gemini key — both apps share Google's project-level quota anyway. -DP=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/dividend-portfolio GEMINI_API_KEY) +ALL=$(kamal secrets fetch --adapter bitwarden --from quantic-prod KAMAL_REGISTRY_PASSWORD LOGO_AUTH_API_KEYS LOGO_AUTH_ADMIN_KEYS GEMINI_API_KEY) -KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) -LOGO_AUTH_API_KEYS=$(kamal secrets extract LOGO_AUTH_API_KEYS ${LOGO}) -LOGO_AUTH_ADMIN_KEYS=$(kamal secrets extract LOGO_AUTH_ADMIN_KEYS ${LOGO}) -LOGO_LLM_GEMINI_API_KEY=$(kamal secrets extract GEMINI_API_KEY ${DP}) +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${ALL}) +LOGO_AUTH_API_KEYS=$(kamal secrets extract LOGO_AUTH_API_KEYS ${ALL}) +LOGO_AUTH_ADMIN_KEYS=$(kamal secrets extract LOGO_AUTH_ADMIN_KEYS ${ALL}) +# Reuse the dividend-portfolio Gemini key — both apps share Google's project-level quota anyway. +LOGO_LLM_GEMINI_API_KEY=$(kamal secrets extract GEMINI_API_KEY ${ALL}) # Anthropic and OpenAI keys stay optional fallbacks — export them in the deploy # shell env to enable, otherwise the chain just skips them. From 4ebdfa5090dfa382268e4fddf64862e219c38f41 Mon Sep 17 00:00:00 2001 From: Francesc Leveque Date: Sun, 24 May 2026 22:04:51 +0200 Subject: [PATCH 3/3] Swap deploy workflow + docs to consumer Bitwarden Replaces the 1Password CLI install step with `npm install -g @bitwarden/cli`, and the `OP_*` env block with `BW_CLIENTID` + `BW_CLIENTSECRET` + `BW_PASSWORD` pulled from repo-level GitHub Actions secrets. env.sample and README updated to reflect the new bootstrap flow. README also documents how to roll back to 1Password using the preserved `.kamal/secrets.1password.example` snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/deploy.yml | 28 ++++++++++++++++++++-------- .kamal/secrets | 10 ++++++---- README.md | 26 ++++++++++++++++++++++---- env.sample | 19 +++++++++++++------ 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0481319..0cc8062 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,12 +17,10 @@ jobs: env: DOCKER_BUILDKIT: 1 - # All deploy secrets are pulled from 1Password by .kamal/secrets. - # OP_SERVICE_ACCOUNT_TOKEN is the only secret; OP_ACCOUNT/OP_VAULT are - # plain config that .kamal/secrets interpolates. - OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - OP_ACCOUNT: my.1password.com - OP_VAULT: quantic-prod + # Bitwarden account email — used by the kamal bitwarden adapter + # (passed as --account in .kamal/secrets). Not strictly secret, but + # kept out of git as a personal-info hygiene measure. + BW_ACCOUNT: ${{ secrets.BW_ACCOUNT }} steps: - name: Checkout code @@ -39,8 +37,22 @@ jobs: # Pinned to match the kamal-proxy version running on the VPS. run: gem install kamal -v 2.7.0 - - name: Install 1Password CLI - uses: 1password/install-cli-action@v3 + - name: Install Bitwarden CLI + run: npm install -g @bitwarden/cli + + # Kamal's bitwarden adapter calls `bw login ` interactively + # (no --apikey support), so we pre-login + pre-unlock here and pass + # BW_SESSION through to the deploy step via GITHUB_ENV. Kamal then + # sees status=unlocked and skips its own login attempt. + - name: Login + unlock Bitwarden vault + env: + BW_CLIENTID: ${{ secrets.BW_CLIENTID }} + BW_CLIENTSECRET: ${{ secrets.BW_CLIENTSECRET }} + BW_PASSWORD: ${{ secrets.BW_PASSWORD }} + run: | + bw config server https://vault.bitwarden.eu + bw login --apikey + echo "BW_SESSION=$(bw unlock --raw --passwordenv BW_PASSWORD)" >> "$GITHUB_ENV" - name: Set up SSH agent uses: webfactory/ssh-agent@v0.9.0 diff --git a/.kamal/secrets b/.kamal/secrets index 0953781..6a164e8 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -1,15 +1,17 @@ # Secrets for Kamal deployment — sourced from Bitwarden Password Manager. # -# Requires BW_CLIENTID, BW_CLIENTSECRET, and BW_PASSWORD in the shell env -# (local: repo .env via direnv; CI: deploy.yml env block). The `bw` CLI must -# be on PATH; kamal's bitwarden adapter handles login + vault unlock. +# Requires BW_ACCOUNT (your Bitwarden login email) and BW_SESSION (obtained +# from `bw unlock --raw --passwordenv BW_PASSWORD`) in the shell env. +# Local: repo .env via direnv (BW_ACCOUNT) + a wrapper that exports +# BW_SESSION before running kamal. CI: deploy.yml does both steps before +# any kamal command runs. The `bw` CLI must be on PATH. # # Parsed by dotenv line-by-line (NOT bash): each $(...) must stay on one line, # and only ${VAR} interpolation is supported (no ${VAR:-default}). # # To switch back to 1Password: see .kamal/secrets.1password.example. -ALL=$(kamal secrets fetch --adapter bitwarden --from quantic-prod KAMAL_REGISTRY_PASSWORD LOGO_AUTH_API_KEYS LOGO_AUTH_ADMIN_KEYS GEMINI_API_KEY) +ALL=$(kamal secrets fetch --adapter bitwarden --account ${BW_ACCOUNT} --from quantic-prod KAMAL_REGISTRY_PASSWORD LOGO_AUTH_API_KEYS LOGO_AUTH_ADMIN_KEYS GEMINI_API_KEY) KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${ALL}) LOGO_AUTH_API_KEYS=$(kamal secrets extract LOGO_AUTH_API_KEYS ${ALL}) diff --git a/README.md b/README.md index 39c4458..5229573 100644 --- a/README.md +++ b/README.md @@ -154,21 +154,39 @@ mise exec ruby@3.4.1 -- kamal deploy ### GitHub Secrets -The deploy workflow needs two repo-level secrets: +The deploy workflow needs these repo-level secrets: | Secret | Purpose | |---|---| | `SSH_PRIVATE_KEY` | Private key for SSH access to the VPS (same key as Pulse / dividend-portfolio) | -| `OP_SERVICE_ACCOUNT_TOKEN` | 1Password service account token — `.kamal/secrets` uses it to fetch the rest at deploy time | +| `BW_ACCOUNT` | Your Bitwarden login email (kept out of git) | +| `BW_CLIENTID` | Bitwarden personal API key — client_id (Account Settings → Security → Keys) | +| `BW_CLIENTSECRET` | Bitwarden personal API key — client_secret | +| `BW_PASSWORD` | Bitwarden master password — used to unlock the vault non-interactively in CI | -`.kamal/secrets` pulls auth keys + LLM keys from a 1Password vault (`OP_VAULT/logo-service`, plus `OP_VAULT/dividend-portfolio` for the shared Gemini key). For local Kamal commands, copy the sample envs and let `direnv` load them: +`.kamal/secrets` pulls auth keys + the shared Gemini key from a single +Bitwarden Secure Note item (`quantic-prod`) holding the values as custom +fields. The deploy workflow installs the `bw` CLI via npm, configures the +EU server, logs in with `bw login --apikey`, unlocks with +`bw unlock --raw --passwordenv BW_PASSWORD`, and exports the resulting +`BW_SESSION` to `$GITHUB_ENV` — kamal then inherits the session and +skips its own login attempt. + +For local Kamal commands, copy the sample envs and let `direnv` load them: ```bash -cp env.sample .env # fill in OP_SERVICE_ACCOUNT_TOKEN +cp env.sample .env # fill in BW_CLIENTID + BW_CLIENTSECRET + BW_PASSWORD cp envrc.sample .envrc direnv allow ``` +#### Switching back to 1Password + +The 1Password version is preserved verbatim at `.kamal/secrets.1password.example`. +To roll back: `cp .kamal/secrets.1password.example .kamal/secrets`, then +revert the BW install + env block in `.github/workflows/deploy.yml`. +The 1P vault was never touched — all secrets are still there. + ### First-time setup on a fresh VPS ```bash diff --git a/env.sample b/env.sample index acc0c31..eb7056c 100644 --- a/env.sample +++ b/env.sample @@ -1,11 +1,18 @@ # Copy to .env (gitignored) and fill in. Loaded by direnv via .envrc. -# === Deploy bootstrap (1Password) === -# All deploy secrets are pulled from 1Password by .kamal/secrets. This token -# is the only one you set yourself. -OP_SERVICE_ACCOUNT_TOKEN=your_1password_service_account_token -OP_ACCOUNT=my.1password.com -OP_VAULT=quantic-prod +# === Deploy bootstrap (Bitwarden) === +# All deploy secrets are pulled from Bitwarden by .kamal/secrets. Set these +# four; the deploy workflow logs in + unlocks the vault and passes a +# BW_SESSION through to kamal. For local kamal use, first run: +# bw config server https://vault.bitwarden.eu # one-time per machine +# bw login --apikey # uses BW_CLIENTID/SECRET +# export BW_SESSION=$(bw unlock --raw --passwordenv BW_PASSWORD) +# Then any `bundle exec kamal ...` call works. +# (To switch back to 1Password, see .kamal/secrets.1password.example.) +BW_ACCOUNT=your_bitwarden_login_email +BW_CLIENTID=your_bitwarden_personal_api_client_id +BW_CLIENTSECRET=your_bitwarden_personal_api_client_secret +BW_PASSWORD=your_bitwarden_master_password # === LLM logo discovery (optional — service works without these) === # Consumed by docker-compose.yml for local LLM-powered logo discovery.