diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 447ee55..726aad7 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 @@ -41,8 +39,22 @@ jobs: # with 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 6086aee..c27c618 100644 --- a/.kamal/secrets +++ b/.kamal/secrets @@ -1,14 +1,18 @@ -# Secrets for Pulse (Elixir/Phoenix app) — sourced from 1Password. +# Secrets for Pulse (Elixir/Phoenix app) — 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) -PULSE=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/pulse SECRET_KEY_BASE LOGO_SERVICE_API_KEY) +ALL=$(kamal secrets fetch --adapter bitwarden --account ${BW_ACCOUNT} --from quantic-prod KAMAL_REGISTRY_PASSWORD SECRET_KEY_BASE LOGO_SERVICE_API_KEY) -KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) -SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${PULSE}) -LOGO_SERVICE_API_KEY=$(kamal secrets extract LOGO_SERVICE_API_KEY ${PULSE}) +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${ALL}) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${ALL}) +LOGO_SERVICE_API_KEY=$(kamal secrets extract LOGO_SERVICE_API_KEY ${ALL}) diff --git a/.kamal/secrets.1password.example b/.kamal/secrets.1password.example new file mode 100644 index 0000000..6086aee --- /dev/null +++ b/.kamal/secrets.1password.example @@ -0,0 +1,14 @@ +# Secrets for Pulse (Elixir/Phoenix app) — 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) +PULSE=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/pulse SECRET_KEY_BASE LOGO_SERVICE_API_KEY) + +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${PULSE}) +LOGO_SERVICE_API_KEY=$(kamal secrets extract LOGO_SERVICE_API_KEY ${PULSE}) diff --git a/.kamal/secrets.beta b/.kamal/secrets.beta index bb358ec..e741f9a 100644 --- a/.kamal/secrets.beta +++ b/.kamal/secrets.beta @@ -1,14 +1,19 @@ -# Secrets for the Pulse beta destination — sourced from 1Password. +# Secrets for the Pulse beta destination — 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). Beta shares prod -# secret values; deploy.beta.yml only overrides env.clear/proxy/volumes. +# Requires BW_CLIENTID, BW_CLIENTSECRET, and BW_PASSWORD in the shell env. +# Beta shares prod secret values (same Bitwarden item) — `deploy.beta.yml` +# only overrides env.clear/proxy/volumes. Beta does NOT fetch +# LOGO_SERVICE_API_KEY (beta-pulse doesn't call logo-service). +# +# If you ever want true env isolation, create a `quantic-beta` Bitwarden +# item with distinct values and swap the `--from quantic-prod` below. # # 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.beta.1password.example. -REGISTRY=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/registry KAMAL_REGISTRY_PASSWORD) -PULSE=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/pulse SECRET_KEY_BASE) +ALL=$(kamal secrets fetch --adapter bitwarden --account ${BW_ACCOUNT} --from quantic-prod KAMAL_REGISTRY_PASSWORD SECRET_KEY_BASE) -KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) -SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${PULSE}) +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${ALL}) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${ALL}) diff --git a/.kamal/secrets.beta.1password.example b/.kamal/secrets.beta.1password.example new file mode 100644 index 0000000..bb358ec --- /dev/null +++ b/.kamal/secrets.beta.1password.example @@ -0,0 +1,14 @@ +# Secrets for the Pulse beta destination — 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). Beta shares prod +# secret values; deploy.beta.yml only overrides env.clear/proxy/volumes. +# +# 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) +PULSE=$(kamal secrets fetch --adapter 1password --account ${OP_ACCOUNT} --from ${OP_VAULT}/pulse SECRET_KEY_BASE) + +KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${REGISTRY}) +SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE ${PULSE}) diff --git a/README.md b/README.md index 999c5ee..4e19a9a 100644 --- a/README.md +++ b/README.md @@ -165,33 +165,53 @@ Auto-deploys on CI success for `main` and `beta` branches. ### GitHub Secrets -The deploy workflow needs just two repository secrets (`Settings > Secrets and variables > Actions`): +The deploy workflow needs these repository secrets (`Settings > Secrets and variables > Actions`): | Secret | Description | |---|---| | `SSH_PRIVATE_KEY` | Private key for SSH access to the VPS (same key as dividend-portfolio) | -| `OP_SERVICE_ACCOUNT_TOKEN` | 1Password service account token — `.kamal/secrets` uses it to fetch all other secrets | - -`KAMAL_REGISTRY_PASSWORD`, `SECRET_KEY_BASE`, and `LOGO_SERVICE_API_KEY` live in a 1Password -vault, fetched at deploy time by `.kamal/secrets` via `kamal secrets fetch --adapter 1password`. -`.github/workflows/deploy.yml` also sets two plain env values, `OP_ACCOUNT` and `OP_VAULT`. - +| `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_REGISTRY_PASSWORD`, `SECRET_KEY_BASE`, and `LOGO_SERVICE_API_KEY` +live in a single Bitwarden Secure Note item (`quantic-prod`) holding the +values as custom fields, fetched at deploy time by `.kamal/secrets` via +`kamal secrets fetch --adapter bitwarden`. 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 files and fill them in (both gitignored; `direnv` loads `.env`): ``` -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` +(and `.kamal/secrets.beta.1password.example`). To roll back: + +```sh +cp .kamal/secrets.1password.example .kamal/secrets +cp .kamal/secrets.beta.1password.example .kamal/secrets.beta +``` + +Then revert the BW install + env block in `.github/workflows/deploy.yml`. +1P vault was never touched — all secrets are still there. + ### Environment Variables -Runtime config, set in `config/deploy.yml` (the secret values among these are fetched from 1Password): +Runtime config, set in `config/deploy.yml` (the secret values among these are fetched from Bitwarden): | Variable | Description | |---|---| -| `SECRET_KEY_BASE` | Phoenix secret — fetched from 1Password | -| `LOGO_SERVICE_API_KEY` | Auth key for the logo service — fetched from 1Password | +| `SECRET_KEY_BASE` | Phoenix secret — fetched from Bitwarden | +| `LOGO_SERVICE_API_KEY` | Auth key for the logo service — fetched from Bitwarden | | `PHX_HOST` | Hostname for URL generation | | `PHX_SERVER` | Set to `true` to start the HTTP server | | `PORT` | HTTP port (default: 4000) | diff --git a/env.sample b/env.sample index 8182324..a7e075d 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. These three -# values are the only ones 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 # === Local dev config ===