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 483e93d..6a164e8 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -1,20 +1,23 @@ -# 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_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. -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 --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 ${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. 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 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.