Skip to content

Commit d857983

Browse files
committed
security: remove hardcoded secrets, add secure defaults and CI/CD workflows
- Replace hardcoded JWT secret/encryption key with env-var loading + auto-generated random fallback - Remove hardcoded admin/admin1234 credentials, add CLI-based admin creation with password policy - Fix deterministic salt in encryption to use os.urandom() - Fix CORS wildcard to configurable origins - Add nightly.yml and weekly.yml CI/CD workflows with security scanning - Update README with security configuration section
1 parent 9f059e7 commit d857983

9 files changed

Lines changed: 289 additions & 37 deletions

File tree

.github/workflows/nightly.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Nightly Build & Test
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *'
6+
workflow_dispatch:
7+
8+
jobs:
9+
nightly:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: ["3.11", "3.12"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python ${{ matrix.python-version }}
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
22+
- name: Install dependencies
23+
run: pip install -e ".[dev]"
24+
25+
- name: Lint
26+
run: ruff check src/ tests/
27+
28+
- name: Type check
29+
run: mypy src/edb/ --ignore-missing-imports
30+
31+
- name: Test with coverage
32+
run: pytest --cov=edb --cov-report=xml --cov-report=term-missing -v
33+
34+
- name: Security scan - secrets
35+
run: |
36+
pip install detect-secrets
37+
detect-secrets scan --all-files --baseline .secrets.baseline || true
38+
39+
- name: Security scan - dependencies
40+
run: |
41+
pip install pip-audit
42+
pip-audit --strict || true
43+
44+
frontend:
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Use Node.js 22.x
50+
uses: actions/setup-node@v4
51+
with:
52+
node-version: 22.x
53+
cache: npm
54+
55+
- name: Install dependencies
56+
run: npm ci
57+
58+
- name: Type check
59+
run: npx tsc --noEmit
60+
61+
- name: Build
62+
run: npm run build

.github/workflows/weekly.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
name: Weekly Comprehensive Tests
2+
3+
on:
4+
schedule:
5+
- cron: '0 6 * * 0'
6+
workflow_dispatch:
7+
8+
jobs:
9+
full-test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: ["3.11", "3.12"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python ${{ matrix.python-version }}
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
22+
- name: Install dependencies
23+
run: pip install -e ".[dev]"
24+
25+
- name: Full test suite with coverage
26+
run: pytest --cov=edb --cov-report=xml --cov-report=term-missing -v --tb=long
27+
28+
- name: Security audit - dependencies
29+
run: |
30+
pip install pip-audit safety
31+
pip-audit --strict
32+
safety check || true
33+
34+
- name: Check for hardcoded secrets
35+
run: |
36+
pip install detect-secrets
37+
detect-secrets scan --all-files --force-use-all-plugins 2>&1 | \
38+
grep -v "ArtifactoryDetector\|AWSKeyDetector" || true
39+
40+
- name: SAST scan
41+
run: |
42+
pip install bandit
43+
bandit -r src/edb/ -ll -ii || true
44+
45+
frontend:
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- name: Use Node.js 22.x
51+
uses: actions/setup-node@v4
52+
with:
53+
node-version: 22.x
54+
cache: npm
55+
56+
- name: Install dependencies
57+
run: npm ci
58+
59+
- name: Type check
60+
run: npx tsc --noEmit
61+
62+
- name: Build
63+
run: npm run build

README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ git clone https://github.com/embeddedos-org/eDB.git
3434
cd eDB
3535
pip install -e ".[dev]"
3636

37-
# Initialize database with default admin
37+
# Initialize database
3838
edb init
3939

40+
# Create admin user (interactive password prompt)
41+
edb admin create --username admin
42+
4043
# Start the server
4144
edb serve --port 8000
4245

@@ -135,8 +138,39 @@ EDB_API_HOST=0.0.0.0
135138
EDB_API_PORT=8000
136139
EDB_JWT_SECRET=your-strong-secret-here
137140
EDB_ENCRYPTION_KEY=your-encryption-key
141+
EDB_CORS_ORIGINS=http://localhost:3000,https://yourdomain.com
142+
```
143+
144+
## Security
145+
146+
### Required Environment Variables for Production
147+
148+
| Variable | Description |
149+
|----------|-------------|
150+
| `EDB_JWT_SECRET` | **Required.** Secret key for JWT signing. If not set, a random key is generated per session (tokens won't persist across restarts). |
151+
| `EDB_ENCRYPTION_KEY` | **Required.** Key for AES-256-GCM encryption at rest. If not set, a random key is generated (encrypted data won't be recoverable after restart). |
152+
| `EDB_CORS_ORIGINS` | Comma-separated allowed origins. Defaults to `http://localhost:3000`. |
153+
154+
### Admin User Setup
155+
156+
Admin users are **not** auto-created. Use the CLI to create one:
157+
158+
```bash
159+
edb admin create --username admin
160+
# You'll be prompted for a password interactively.
161+
# Password must be 12+ chars with uppercase, lowercase, digit, and special char.
138162
```
139163

164+
### Security Best Practices
165+
166+
- Always set `EDB_JWT_SECRET` and `EDB_ENCRYPTION_KEY` in production
167+
- Use strong, unique values (e.g., `openssl rand -base64 48`)
168+
- Never commit `.env` files to version control
169+
- Restrict CORS origins to your actual frontend domains
170+
- Review audit logs regularly via `/admin/audit` endpoint
171+
172+
See [SECURITY.md](SECURITY.md) for vulnerability reporting.
173+
140174
## Development
141175

142176
### Python Backend
@@ -180,10 +214,6 @@ ruff format src/ tests/
180214

181215
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
182216

183-
## Security
184-
185-
See [SECURITY.md](SECURITY.md) for vulnerability reporting and security best practices.
186-
187217
## License
188218

189219
MIT License. See [LICENSE](LICENSE) for details.

src/edb/api/app.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from edb.api.routes import admin, auth, documents, ebot, graph, kv, sql
1515
from edb.config import EDBConfig
1616

17+
logger = logging.getLogger("edb.api")
18+
1719

1820
@asynccontextmanager
1921
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -23,8 +25,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
2325

2426
logging.basicConfig(level=getattr(logging, config.log_level.upper(), logging.INFO))
2527

26-
if config.create_admin:
27-
state.user_manager.ensure_admin_exists()
2828
yield
2929
state.database.close()
3030

@@ -40,12 +40,13 @@ def create_app(config: EDBConfig | None = None) -> FastAPI:
4040
lifespan=lifespan,
4141
)
4242

43+
origins = [o.strip() for o in config.cors_origins.split(",") if o.strip()]
4344
app.add_middleware(
4445
CORSMiddleware,
45-
allow_origins=["*"],
46+
allow_origins=origins,
4647
allow_credentials=True,
47-
allow_methods=["*"],
48-
allow_headers=["*"],
48+
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
49+
allow_headers=["Authorization", "Content-Type"],
4950
)
5051

5152
if config.rate_limit_enabled:

src/edb/auth/jwt_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class JWTHandler:
1616

1717
def __init__(
1818
self,
19-
secret_key: str = "edb-secret-change-me-in-production",
19+
secret_key: str,
2020
algorithm: str = "HS256",
2121
access_token_expire_minutes: int = 60,
2222
refresh_token_expire_days: int = 7,

src/edb/auth/users.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6+
import re
67
import uuid
78
from datetime import UTC, datetime
89
from typing import Any
@@ -15,10 +16,28 @@
1516
USERS_TABLE = "_edb_users"
1617
logger = logging.getLogger("edb.auth.users")
1718

18-
19-
20-
21-
USERS_TABLE = "_edb_users"
19+
MIN_PASSWORD_LENGTH = 12
20+
PASSWORD_POLICY = (
21+
"Password must be at least 12 characters and contain uppercase, "
22+
"lowercase, digit, and special character."
23+
)
24+
25+
26+
def validate_password_strength(password: str) -> None:
27+
"""Enforce password complexity requirements."""
28+
errors: list[str] = []
29+
if len(password) < MIN_PASSWORD_LENGTH:
30+
errors.append(f"at least {MIN_PASSWORD_LENGTH} characters")
31+
if not re.search(r"[A-Z]", password):
32+
errors.append("an uppercase letter")
33+
if not re.search(r"[a-z]", password):
34+
errors.append("a lowercase letter")
35+
if not re.search(r"\d", password):
36+
errors.append("a digit")
37+
if not re.search(r"[!@#$%^&*(),.?\":{}|<>\-_=+\[\]\\;'/`~]", password):
38+
errors.append("a special character")
39+
if errors:
40+
raise ValueError(f"Password too weak — must contain: {', '.join(errors)}")
2241

2342

2443
class UserManager:
@@ -42,11 +61,14 @@ def _ensure_table(self) -> None:
4261
""")
4362
self._engine.commit()
4463

45-
def create_user(self, user_data: UserCreate) -> UserResponse:
64+
def create_user(self, user_data: UserCreate, skip_password_check: bool = False) -> UserResponse:
4665
"""Create a new user with hashed password."""
4766
if self.get_by_username(user_data.username):
4867
raise ValueError(f"Username '{user_data.username}' already exists")
4968

69+
if not skip_password_check:
70+
validate_password_strength(user_data.password)
71+
5072
user_id = str(uuid.uuid4())
5173
now = datetime.now(UTC).isoformat()
5274
password_hash = self._hash_password(user_data.password)
@@ -124,17 +146,20 @@ def deactivate_user(self, user_id: str) -> bool:
124146
self._engine.commit()
125147
return cursor.rowcount > 0
126148

127-
def ensure_admin_exists(self) -> None:
128-
"""Create a default admin user if no admins exist."""
149+
def ensure_admin_exists(self, username: str, password: str) -> None:
150+
"""Create an admin user with the provided credentials if no admins exist."""
151+
validate_password_strength(password)
129152
row = self._engine.fetchone(
130153
f'SELECT id FROM "{USERS_TABLE}" WHERE role = ?', (Role.ADMIN.value,)
131154
)
132155
if row is None:
133-
self.create_user(UserCreate(
134-
username="admin",
135-
password="admin1234",
136-
role=Role.ADMIN,
137-
))
156+
self.create_user(
157+
UserCreate(username=username, password=password, role=Role.ADMIN),
158+
skip_password_check=True,
159+
)
160+
logger.info("Admin user '%s' created.", username)
161+
else:
162+
logger.info("Admin user already exists, skipping creation.")
138163

139164
def change_password(self, user_id: str, current_password: str, new_password: str) -> bool:
140165
"""Change a user password after verifying the current one."""
@@ -143,6 +168,7 @@ def change_password(self, user_id: str, current_password: str, new_password: str
143168
return False
144169
if not self._verify_password(current_password, user.password_hash):
145170
return False
171+
validate_password_strength(new_password)
146172
new_hash = self._hash_password(new_password)
147173
now = datetime.now(UTC).isoformat()
148174
cursor = self._engine.execute(

0 commit comments

Comments
 (0)