Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <email>` 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
Expand Down
20 changes: 12 additions & 8 deletions .kamal/secrets
Original file line number Diff line number Diff line change
@@ -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})
14 changes: 14 additions & 0 deletions .kamal/secrets.1password.example
Original file line number Diff line number Diff line change
@@ -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})
21 changes: 13 additions & 8 deletions .kamal/secrets.beta
Original file line number Diff line number Diff line change
@@ -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})
14 changes: 14 additions & 0 deletions .kamal/secrets.beta.1password.example
Original file line number Diff line number Diff line change
@@ -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})
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
19 changes: 13 additions & 6 deletions env.sample
Original file line number Diff line number Diff line change
@@ -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 ===

Expand Down
Loading