# OSINT Surface Reduction

## Context

After deploying security hardening (Redis auth, NetworkPolicies, CORS, security headers), the next question is: what can an external attacker learn just by probing the public endpoints? The exploration found several categories of information leakage. Some are intentional (public substance info for harm reduction), some are not.

This plan covers the unintentional leakage -- things that help an attacker map your infrastructure, identify software versions, or craft targeted attacks.

## Findings & Recommendations

### 1. Error messages leak internals (HIGH)

**Current:** Exception messages are returned verbatim in HTTP responses.

API (`router.cr:180-184`):
```
"Internal server error: Connection refused to logs-api-redis:6379"
```

Heartbeat (`errors.go:42-47`):
```
"500 Database error: dial tcp heartbeat-redis:6379: connection refused"
```

**Fix:** Return generic error messages. Log the real error server-side only.

- `health/services/core/src/router.cr` -- global rescue block
- `health/services/core/src/handlers/substance_handler.cr` -- handler-level rescues
- `health/heartbeat/errors.go` -- HandleInternalErr, HandleClientErr

### 2. Health endpoint exposes backend topology (MEDIUM)

**Current:** `/health` returns:
```json
{"status":"ok","service":"logs-api-redis","redis_connection":"connected"}
```

The `service` field reveals the internal Redis service name. The error case is worse -- it returns `ex.message` which can include connection strings.

**Fix:** Return `{"status":"ok"}` on success, `{"status":"error"}` on failure. No service names, no error details.

- `health/services/core/src/router.cr` lines 255-259

### 3. Version string in API responses (LOW)

**Current:** `/v1/` and `/v1/info` return `"version":"1.0.0"`.

**Fix:** Remove the version field, or keep it if you want it public (it's a static string, not a real semver that changes, so risk is low).

- `health/services/core/src/router.cr` lines 266, 287

### 4. Git commit hash on heartbeat page (LOW)

**Current:** Heartbeat renders the exact git commit SHA on the public page and links to the GitHub repo.

**Fix:** Remove or replace with a generic string. The GitHub repo link is fine if the repo is public.

- `health/heartbeat/templates/mainpage.qtpl` line 54

### 5. OpenAPI spec publicly accessible (MEDIUM)

**Current:** Full API schema available unauthenticated at `/v1/openapi.yaml` and Swagger UI at `/`. Documents every endpoint, auth mechanisms (`jhapi_` bearer prefix), webhook callback patterns, and internal/external server URLs.

**Fix:** Gate behind auth, or accept the risk if you consider it documentation for legitimate users.

- `health/services/core/src/router.cr` -- routes serving openapi.yaml and swagger-ui.html

### 6. Rate limit headers expose config (LOW)

**Current:** `X-RateLimit-Remaining` and `X-RateLimit-Limit` headers reveal exact bucket size and current state.

**Fix:** Remove these headers, or keep them (they're standard practice and the risk is marginal -- an attacker can empirically discover rate limits anyway).

- `health/services/core/src/router.cr` lines 92-93

### 7. Repo hygiene -- credentials in tracked files (MEDIUM)

**Current:**
- `services/compose.yml` -- contains `REDIS_PASSWORD=foobar` and `API_PASSWORD=pissword` (development values, tracked in git)
- `services/core/test_raw_dose_live.sh` -- contains `AUTH="admin:admin_password_change_me"` (old default, tracked)
- `services/core/test_raw_dose.sh` -- same

These are development/test values, not production credentials, but they're still in the repo. If someone finds the repo they learn the credential format and can try these against production.

**Fix:** Move to `.env` files (already gitignored) and source from there. Or leave as-is if the repo is private and these aren't production credentials.

## Recommended Scope

Items 1 and 2 are the most impactful -- they leak real runtime information that changes based on infrastructure state. The rest are static information with lower risk.

**Proposed changes (3 files):**

| File | Change |
|------|--------|
| `health/services/core/src/router.cr` | Generic error messages in global rescue; strip service name from `/health`; optionally remove version |
| `health/services/core/src/handlers/substance_handler.cr` | Generic error messages in handler rescues |
| `health/heartbeat/errors.go` | Generic error messages in HandleInternalErr/HandleClientErr |

## Verification

After changes, the smoketest.sh already covers the health endpoint. Add manual verification:

```bash
# Verify error responses don't leak details (hit a bad endpoint)
curl -s https://api.josie.health/v1/nonexistent | jq .

# Verify health endpoint is sanitized
curl -s https://api.josie.health/health | jq .

# Verify heartbeat doesn't show git hash (visual check)
curl -s https://heartbeat.josie.health/ | grep -i "version\|commit\|git"
```
