security: rotate leaked Signal API key, remove it from frontend + source
The backend master SIGNAL_API_KEY had leaked via two paths: - baked into the public Vite bundle via VITE_SIGNAL_API_KEY (Vercel) - hardcoded as a fallback in scripts/signal_e2e_test.py The key has been rotated on Railway (old value now returns 401) and the VITE_SIGNAL_API_KEY var removed from Vercel. This commit removes the two code paths so the master key can never be re-baked or re-committed: - signal-ui/src/lib/api.js: drop the X-API-Key fallback entirely; the frontend now authenticates only with the signed-in user's Clerk token - scripts/signal_e2e_test.py: read SIGNAL_API_KEY from env only; exit if unset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70c1f28230
commit
25f65326d4
2 changed files with 11 additions and 7 deletions
|
|
@ -8,7 +8,7 @@ Usage:
|
||||||
python3 scripts/signal_e2e_test.py
|
python3 scripts/signal_e2e_test.py
|
||||||
python3 scripts/signal_e2e_test.py --url https://signal-api-production-91c2.up.railway.app
|
python3 scripts/signal_e2e_test.py --url https://signal-api-production-91c2.up.railway.app
|
||||||
|
|
||||||
Authentication: API key from env var SIGNAL_API_KEY or hardcoded fallback.
|
Authentication: API key read from the SIGNAL_API_KEY environment variable (required).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
@ -27,7 +27,9 @@ REPO_ROOT = Path(__file__).parent.parent
|
||||||
TEST_DATA = REPO_ROOT / "test-data"
|
TEST_DATA = REPO_ROOT / "test-data"
|
||||||
|
|
||||||
DEFAULT_URL = "https://signal-api-production-91c2.up.railway.app"
|
DEFAULT_URL = "https://signal-api-production-91c2.up.railway.app"
|
||||||
API_KEY = os.getenv("SIGNAL_API_KEY", "4HKamqzn78Md-L-xj2OjZESVwZh2Otzc6iFRyqVZl7U")
|
API_KEY = os.getenv("SIGNAL_API_KEY", "")
|
||||||
|
if not API_KEY:
|
||||||
|
sys.exit("SIGNAL_API_KEY is not set. Export the current backend key before running this test.")
|
||||||
|
|
||||||
PASS = "PASS"
|
PASS = "PASS"
|
||||||
FAIL = "FAIL"
|
FAIL = "FAIL"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
|
|
||||||
const BACKEND_URL = import.meta.env.VITE_SIGNAL_BACKEND_URL ||
|
const BACKEND_URL = import.meta.env.VITE_SIGNAL_BACKEND_URL ||
|
||||||
"https://signal-api-production-91c2.up.railway.app";
|
"https://signal-api-production-91c2.up.railway.app";
|
||||||
const API_KEY = import.meta.env.VITE_SIGNAL_API_KEY || "";
|
// The frontend authenticates only with the signed-in user's Clerk token.
|
||||||
|
// It must never carry a service API key — that key would be baked into the
|
||||||
|
// public bundle and grant full backend access to anyone who views the source.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload a CSV file to the backend scoring endpoint.
|
* Upload a CSV file to the backend scoring endpoint.
|
||||||
|
|
@ -17,7 +19,7 @@ export async function uploadToBackend(file, token = null) {
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
const authHeader = token
|
const authHeader = token
|
||||||
? { "Authorization": `Bearer ${token}` }
|
? { "Authorization": `Bearer ${token}` }
|
||||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
: {};
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${BACKEND_URL}/api/upload`, {
|
const resp = await fetch(`${BACKEND_URL}/api/upload`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -42,7 +44,7 @@ export async function uploadToBackend(file, token = null) {
|
||||||
export async function exportFromBackend(records, batchId = null, token = null) {
|
export async function exportFromBackend(records, batchId = null, token = null) {
|
||||||
const authHeader = token
|
const authHeader = token
|
||||||
? { "Authorization": `Bearer ${token}` }
|
? { "Authorization": `Bearer ${token}` }
|
||||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
: {};
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${BACKEND_URL}/api/export`, {
|
const resp = await fetch(`${BACKEND_URL}/api/export`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -106,7 +108,7 @@ export function apiRecordToLocal(r) {
|
||||||
export async function confirmVisit(payload, token = null) {
|
export async function confirmVisit(payload, token = null) {
|
||||||
const authHeader = token
|
const authHeader = token
|
||||||
? { "Authorization": `Bearer ${token}` }
|
? { "Authorization": `Bearer ${token}` }
|
||||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
: {};
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, {
|
const res = await fetch(`${BACKEND_URL}/api/confirm-visit`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -142,7 +144,7 @@ export async function confirmVisit(payload, token = null) {
|
||||||
export async function updateDocStatus(patientId, docType, status, statusDate = null, expiryDate = null, token = null) {
|
export async function updateDocStatus(patientId, docType, status, statusDate = null, expiryDate = null, token = null) {
|
||||||
const authHeader = token
|
const authHeader = token
|
||||||
? { "Authorization": `Bearer ${token}` }
|
? { "Authorization": `Bearer ${token}` }
|
||||||
: API_KEY ? { "X-API-Key": API_KEY } : {};
|
: {};
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${BACKEND_URL}/api/doc-status`, {
|
const resp = await fetch(`${BACKEND_URL}/api/doc-status`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue