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 @@ -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 <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
25 changes: 14 additions & 11 deletions .kamal/secrets
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
22 changes: 22 additions & 0 deletions .kamal/secrets.1password.example
Original file line number Diff line number Diff line change
@@ -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
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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. 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.
Expand Down
Loading