Skip to content

Commit c8247b1

Browse files
committed
fix(security): remove jwt secret from repo
1 parent 84a8779 commit c8247b1

5 files changed

Lines changed: 30 additions & 4 deletions

File tree

.github/workflows/deploy-server.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
8787
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
8888
DEPLOY_SERVICE_NAME: ${{ secrets.DEPLOY_SERVICE_NAME }}
89+
DEPLOY_JWT_SECRET: ${{ secrets.DEPLOY_JWT_SECRET }}
8990
run: |
9091
ssh_opts=(
9192
-o BatchMode=yes
@@ -99,6 +100,7 @@ jobs:
99100
printf 'export DEPLOY_ARCHIVE=%q\n' "$REMOTE_ARCHIVE"
100101
printf 'export DEPLOY_PATH=%q\n' "$DEPLOY_PATH"
101102
printf 'export DEPLOY_SERVICE_NAME=%q\n' "$DEPLOY_SERVICE_NAME"
103+
printf 'export DEPLOY_JWT_SECRET=%q\n' "$DEPLOY_JWT_SECRET"
102104
cat scripts/deploy/server-release.sh
103105
} | ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s"
104106

clipSync-server/configs/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ http_port: 8081
1010
db_path: ./data/clipsync.db
1111

1212
# JWT secret key (CHANGE IN PRODUCTION)
13-
jwt_secret: "3lGXXmAhFIfnMHBOClLSECwOjRc5V77rsyu1v4aikgZu1TRQLUZOpf9BbeJWP8IG"
13+
jwt_secret: "clipsync-secret-change-in-production"
1414

1515
# JWT token expiry in hours (720 = 30 days)
1616
jwt_expiry_hours: 720

clipSync-server/internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"gopkg.in/yaml.v3"
89
)
@@ -55,6 +56,10 @@ func Load(path string) (Config, error) {
5556
return cfg, fmt.Errorf("parse config file: %w", err)
5657
}
5758

59+
if envSecret := strings.TrimSpace(os.Getenv("CLIPSYNC_JWT_SECRET")); envSecret != "" {
60+
cfg.JWTSecret = envSecret
61+
}
62+
5863
return cfg, nil
5964
}
6065

docs/deployment/github-actions-server.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ Configure these repository secrets before enabling the workflow:
2727
| `DEPLOY_PATH` | `/opt/clipSync-server-src` | Live server directory that receives `bin/`, `configs/`, and preserved `data/` |
2828
| `DEPLOY_SERVICE_NAME` | `clipsync.service` | systemd service restarted during deploy and rollback |
2929
| `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -H 8.141.100.238` | Host key entry used for strict SSH host verification |
30+
| `DEPLOY_JWT_SECRET` | a long random secret | Injected into the deployed server config during deployment; do not store the live JWT secret in git |
3031

3132
Notes:
3233

3334
- `DEPLOY_SSH_KEY` must match the public key installed for `DEPLOY_USER` on the server.
3435
- `DEPLOY_KNOWN_HOSTS` is required because the workflow uses `StrictHostKeyChecking=yes`.
3536
- Keep `DEPLOY_PATH` and `DEPLOY_SERVICE_NAME` aligned with the actual server layout and systemd unit.
37+
- `DEPLOY_JWT_SECRET` should be treated as the real production JWT signing key. The repository config now keeps only the placeholder value.
3638
- Optional: `DEPLOY_PUBLIC_HEALTH_URL` can override the final GitHub Actions health-check URL when the public endpoint differs from `http://<DEPLOY_HOST>:8081/api/v1/health`.
3739

3840
## Server Requirements
@@ -60,7 +62,7 @@ The server process defaults to the relative path `configs/config.yaml`. That mea
6062

6163
## First-Time Setup
6264

63-
1. Confirm the repository version of `clipSync-server/configs/config.yaml` is safe for the deployment environment.
65+
1. Confirm the repository version of `clipSync-server/configs/config.yaml` keeps the placeholder JWT secret and any other non-secret defaults you want deployed.
6466
2. Create the required GitHub repository secrets listed above.
6567
3. Install the deploy public key for `DEPLOY_USER` on the server.
6668
4. Capture the server host key for `DEPLOY_KNOWN_HOSTS`.
@@ -99,7 +101,7 @@ curl --fail http://127.0.0.1:8081/api/v1/health
99101
The remote deployment script is intentionally narrow and opinionated:
100102

101103
- `data/` is preserved. The script creates `DEPLOY_PATH/data` if needed and never deletes or replaces it.
102-
- `configs/config.yaml` is overwritten from the repository on every deploy.
104+
- `configs/config.yaml` is overwritten from the repository on every deploy, then the script replaces the placeholder JWT secret with `DEPLOY_JWT_SECRET` on the server.
103105
- The live binary path is `DEPLOY_PATH/bin/clipsync-server-linux`.
104106
- The binary backup path is `DEPLOY_PATH/bin/clipsync-server-linux.prev`.
105107
- The config backup path is `DEPLOY_PATH/configs/config.yaml.prev`.
@@ -168,6 +170,7 @@ What to verify:
168170

169171
- The service name in `DEPLOY_SERVICE_NAME` is correct.
170172
- The deployed config in `/opt/clipSync-server-src/configs/config.yaml` contains production-safe values.
173+
- `DEPLOY_JWT_SECRET` exists in GitHub Secrets and the deployed config no longer contains the placeholder value.
171174
- The service is actually starting the binary at `/opt/clipSync-server-src/bin/clipsync-server-linux`.
172175
- Port `8081` is listening and reachable from outside the host if the final GitHub Actions health check is using the default URL.
173176
- If `DEPLOY_PUBLIC_HEALTH_URL` is configured, verify that public endpoint and any proxy/load-balancer routing in front of it.
@@ -178,7 +181,7 @@ What to verify:
178181
Before treating this flow as ready:
179182

180183
- Confirm the secrets in GitHub match the real server.
181-
- Confirm `clipSync-server/configs/config.yaml` is intended to overwrite production on every push to `main`.
184+
- Confirm `clipSync-server/configs/config.yaml` is intended to overwrite production on every push to `main`, aside from the JWT secret placeholder that gets replaced at deploy time.
182185
- Confirm `DEPLOY_PUBLIC_HEALTH_URL` is set if the API port is not publicly reachable from GitHub-hosted runners.
183186
- Confirm the systemd service still uses the same binary path and health endpoint.
184187
- Confirm operators understand that `data/` is preserved but config is not, and that rollback does not restore SQLite database state.

scripts/deploy/server-release.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ resolve_release_root() {
138138
require_env DEPLOY_ARCHIVE
139139
require_env DEPLOY_PATH
140140
require_env DEPLOY_SERVICE_NAME
141+
require_env DEPLOY_JWT_SECRET
141142

142143
DEPLOY_HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:8081/api/v1/health}"
143144

@@ -190,6 +191,21 @@ trap 'rollback_on_error "$?"' ERR
190191
install -m 0755 "$NEW_BINARY" "$TEMP_BINARY"
191192
mv -f "$TEMP_BINARY" "$LIVE_BINARY"
192193
install -m 0644 "$NEW_CONFIG" "$LIVE_CONFIG"
194+
python3 - "$LIVE_CONFIG" "$DEPLOY_JWT_SECRET" <<'PY'
195+
import pathlib
196+
import sys
197+
198+
config_path = pathlib.Path(sys.argv[1])
199+
jwt_secret = sys.argv[2]
200+
201+
content = config_path.read_text(encoding="utf-8")
202+
needle = 'jwt_secret: "clipsync-secret-change-in-production"'
203+
204+
if needle not in content:
205+
raise SystemExit("Unable to locate jwt_secret placeholder in deployed config")
206+
207+
config_path.write_text(content.replace(needle, f'jwt_secret: "{jwt_secret}"', 1), encoding="utf-8")
208+
PY
193209

194210
if ! systemctl restart "$DEPLOY_SERVICE_NAME"; then
195211
echo "Service restart failed: $DEPLOY_SERVICE_NAME" >&2

0 commit comments

Comments
 (0)