Skip to content

Commit b032c98

Browse files
authored
chore: harden env secrets and docker health checks (#1)
1 parent c76aa3c commit b032c98

7 files changed

Lines changed: 49 additions & 27 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ APP_ENV=development
33
DATABASE_URL=sqlite:////app/data/dev.sqlite3
44
JWT_SECRET_KEY=replace-with-strong-random-secret-min-32-chars
55
BIOMETRIC_INGEST_API_KEY=replace-with-strong-random-api-key
6+
JWT_EXPIRE_MINUTES=60
67

78
DEFAULT_ADMIN_EMAIL=admin@company.com
89
DEFAULT_ADMIN_PASSWORD=replace-admin-password

backend/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ JWT_ALGORITHM=HS256
99
JWT_EXPIRE_MINUTES=60
1010

1111
DEFAULT_ADMIN_EMAIL=admin@company.com
12-
DEFAULT_ADMIN_PASSWORD=replace-admin-password
12+
DEFAULT_ADMIN_PASSWORD=replace-admin-password-min-12-chars
1313
DEFAULT_ADMIN_NAME=System Admin
1414
DEFAULT_HR_EMAIL=hr@company.com
15-
DEFAULT_HR_PASSWORD=replace-hr-password
15+
DEFAULT_HR_PASSWORD=replace-hr-password-min-12-chars
1616
DEFAULT_HR_NAME=HR Manager
1717

1818
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:8080

backend/app/config.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,32 @@ class Settings(BaseSettings):
99

1010
database_url: str = "sqlite:///./dev.sqlite3"
1111

12-
jwt_secret_key: str = "change-me-in-production"
12+
jwt_secret_key: str
1313
jwt_algorithm: str = "HS256"
1414
jwt_expire_minutes: int = 60
1515

1616
default_admin_email: str = "admin@company.com"
17-
default_admin_password: str = "admin12345"
17+
default_admin_password: str
1818
default_admin_name: str = "System Admin"
1919
default_hr_email: str = "hr@company.com"
20-
default_hr_password: str = "hr12345"
20+
default_hr_password: str
2121
default_hr_name: str = "HR Manager"
2222

2323
cors_origins: str = "http://127.0.0.1:5173,http://localhost:5173"
24-
biometric_ingest_api_key: str = "local-biometric-key"
24+
biometric_ingest_api_key: str
2525

2626
@model_validator(mode="after")
2727
def validate_security_settings(self) -> "Settings":
28-
if self.app_env.lower() == "production":
29-
if self.jwt_secret_key == "change-me-in-production" or len(self.jwt_secret_key) < 32:
30-
raise ValueError("JWT_SECRET_KEY must be set to a strong value in production")
31-
if self.default_admin_password == "admin12345" or self.default_hr_password == "hr12345":
32-
raise ValueError("Default account passwords must be changed in production")
33-
if self.biometric_ingest_api_key == "local-biometric-key" or len(self.biometric_ingest_api_key) < 16:
34-
raise ValueError("BIOMETRIC_INGEST_API_KEY must be set to a strong value in production")
28+
if len(self.jwt_secret_key) < 32:
29+
raise ValueError("JWT_SECRET_KEY must be at least 32 characters")
30+
if len(self.biometric_ingest_api_key) < 16:
31+
raise ValueError("BIOMETRIC_INGEST_API_KEY must be at least 16 characters")
32+
if self.default_admin_password in {"admin12345", "change-me"}:
33+
raise ValueError("DEFAULT_ADMIN_PASSWORD must not use weak default values")
34+
if self.default_hr_password in {"hr12345", "change-me"}:
35+
raise ValueError("DEFAULT_HR_PASSWORD must not use weak default values")
36+
if self.app_env.lower() == "production" and self.default_admin_password == self.default_hr_password:
37+
raise ValueError("DEFAULT_ADMIN_PASSWORD and DEFAULT_HR_PASSWORD must be different in production")
3538
return self
3639

3740
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")

backend/tests/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66

77

88
os.environ['DATABASE_URL'] = 'sqlite:///./test_hr.sqlite3'
9+
os.environ['JWT_SECRET_KEY'] = 'test_jwt_secret_3d9f6a1b2c4e8g7h5k0m9n2p6q1r4s8'
10+
os.environ['BIOMETRIC_INGEST_API_KEY'] = 'test_bio_ingest_2f3a7c8d'
911
os.environ['DEFAULT_ADMIN_EMAIL'] = 'admin@company.com'
10-
os.environ['DEFAULT_ADMIN_PASSWORD'] = 'admin12345'
12+
os.environ['DEFAULT_ADMIN_PASSWORD'] = 'admin987654321'
1113
os.environ['DEFAULT_ADMIN_NAME'] = 'System Admin'
14+
os.environ['DEFAULT_HR_EMAIL'] = 'hr@company.com'
15+
os.environ['DEFAULT_HR_PASSWORD'] = 'hr987654321'
16+
os.environ['DEFAULT_HR_NAME'] = 'HR Manager'
1217

1318
from app.database import Base, engine # noqa: E402
1419
from app.main import app # noqa: E402
@@ -30,7 +35,7 @@ def auth_headers(client: TestClient) -> dict[str, str]:
3035
'/api/v1/auth/login',
3136
json={
3237
'email': 'admin@company.com',
33-
'password': 'admin12345',
38+
'password': 'admin987654321',
3439
},
3540
)
3641
token = login_response.json()['data']['access_token']

backend/tests/test_attendance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_biometric_ingest_and_payroll_attendance_integration(client: TestClient)
3333

3434
ingest_response = client.post(
3535
'/api/v1/leave/attendance/punches/ingest',
36-
headers={'x-biometric-api-key': 'local-biometric-key'},
36+
headers={'x-biometric-api-key': 'test_bio_ingest_2f3a7c8d'},
3737
json={
3838
'punches': [
3939
{
@@ -58,7 +58,7 @@ def test_biometric_ingest_and_payroll_attendance_integration(client: TestClient)
5858

5959
duplicate_ingest_response = client.post(
6060
'/api/v1/leave/attendance/punches/ingest',
61-
headers={'x-biometric-api-key': 'local-biometric-key'},
61+
headers={'x-biometric-api-key': 'test_bio_ingest_2f3a7c8d'},
6262
json={
6363
'punches': [
6464
{

backend/tests/test_auth.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def test_login_success(client: TestClient):
88
'/api/v1/auth/login',
99
json={
1010
'email': 'admin@company.com',
11-
'password': 'admin12345',
11+
'password': 'admin987654321',
1212
},
1313
)
1414

@@ -85,8 +85,8 @@ def test_change_password(client: TestClient):
8585
'/api/v1/auth/change-password',
8686
headers=headers,
8787
json={
88-
'current_password': 'admin12345',
89-
'new_password': 'admin12345-new',
88+
'current_password': 'admin987654321',
89+
'new_password': 'admin987654321-new',
9090
},
9191
)
9292
assert change_password_response.status_code == 200
@@ -95,7 +95,7 @@ def test_change_password(client: TestClient):
9595
'/api/v1/auth/login',
9696
json={
9797
'email': 'admin@company.com',
98-
'password': 'admin12345-new',
98+
'password': 'admin987654321-new',
9999
},
100100
)
101101
assert login_with_new_password_response.status_code == 200

docker-compose.yml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,33 @@ services:
88
APP_ENV: ${APP_ENV:-development}
99
APP_PORT: 8000
1010
DATABASE_URL: ${DATABASE_URL:-sqlite:////app/data/dev.sqlite3}
11-
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-me-in-production}
11+
JWT_SECRET_KEY: ${JWT_SECRET_KEY?JWT_SECRET_KEY is required}
1212
JWT_ALGORITHM: HS256
13-
JWT_EXPIRE_MINUTES: 60
13+
JWT_EXPIRE_MINUTES: ${JWT_EXPIRE_MINUTES:-60}
1414
DEFAULT_ADMIN_EMAIL: ${DEFAULT_ADMIN_EMAIL:-admin@company.com}
15-
DEFAULT_ADMIN_PASSWORD: ${DEFAULT_ADMIN_PASSWORD:-admin12345}
15+
DEFAULT_ADMIN_PASSWORD: ${DEFAULT_ADMIN_PASSWORD?DEFAULT_ADMIN_PASSWORD is required}
1616
DEFAULT_ADMIN_NAME: ${DEFAULT_ADMIN_NAME:-System Admin}
1717
DEFAULT_HR_EMAIL: ${DEFAULT_HR_EMAIL:-hr@company.com}
18-
DEFAULT_HR_PASSWORD: ${DEFAULT_HR_PASSWORD:-hr12345}
18+
DEFAULT_HR_PASSWORD: ${DEFAULT_HR_PASSWORD?DEFAULT_HR_PASSWORD is required}
1919
DEFAULT_HR_NAME: ${DEFAULT_HR_NAME:-HR Manager}
2020
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:8080}
21-
BIOMETRIC_INGEST_API_KEY: ${BIOMETRIC_INGEST_API_KEY:-local-biometric-key}
21+
BIOMETRIC_INGEST_API_KEY: ${BIOMETRIC_INGEST_API_KEY?BIOMETRIC_INGEST_API_KEY is required}
2222
volumes:
2323
- backend-data:/app/data
2424
expose:
2525
- "8000"
26+
healthcheck:
27+
test:
28+
[
29+
"CMD",
30+
"python",
31+
"-c",
32+
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/', timeout=5)",
33+
]
34+
interval: 10s
35+
timeout: 5s
36+
retries: 6
37+
start_period: 10s
2638
restart: unless-stopped
2739

2840
frontend:
@@ -35,7 +47,8 @@ services:
3547
ports:
3648
- "8080:80"
3749
depends_on:
38-
- backend
50+
backend:
51+
condition: service_healthy
3952
restart: unless-stopped
4053

4154
volumes:

0 commit comments

Comments
 (0)