Skip to content

Commit eecba34

Browse files
authored
Merge pull request #346 from Pseudo-Lab/fix/getcloser-security-config
Fix/getcloser security config
2 parents 7dd6ab4 + 21981ae commit eecba34

File tree

9 files changed

+88
-7
lines changed

9 files changed

+88
-7
lines changed

.github/workflows/deploy-getcloser.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ jobs:
4747
echo "TEAM_SIZE=${{ vars.TEAM_SIZE}}" >> .env
4848
echo "PENDING_TIMEOUT_MINUTES=${{ vars.PENDING_TIMEOUT_MINUTES}}" >> .env
4949
echo "DATA_DIR_HOST=${{ vars.DATA_DIR_HOST }}" >> .env
50+
echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env
51+
echo "ENVIRONMENT=prod" >> .env
5052
5153
- name: 🚀 Deploy to PROD
5254
run: |

.github/workflows/devfactory-homepage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
cat > .env <<'EOF'
3333
APP_HOST=${{ vars.APP_HOST }}
3434
DATABASE_URL=${{ secrets.DATABASE_URL }}
35+
ACCESS_LOGGING_IP_SALT=${{ secrets.ACCESS_LOGGING_IP_SALT }}
3536
EOF
3637
3738
- name: Build & up (prod)

cert/backend/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# LOG_LEVEL=INFO
2+
# ENVIRONMENT=dev
3+
4+
# CORS Origins (쉼표로 구분)
5+
CORS_ORIGINS=https://cert.pseudo-lab.com,https://dev-cert.pseudolab-devfactory.com,http://localhost:5173

cert/backend/src/main.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,23 @@ def configure_logging() -> None:
4040
# Access log middleware
4141
app.middleware("http")(access_log_middleware)
4242

43-
# CORS 미들웨어 설정
44-
origins = os.getenv("CORS_ORIGINS", "").split(",")
43+
# CORS configuration
44+
# Load allowed origins from CORS_ORIGINS environment variable (comma-separated)
45+
cors_origins_str = os.getenv("CORS_ORIGINS", "")
46+
if cors_origins_str:
47+
origins = [origin.strip() for origin in cors_origins_str.split(",") if origin.strip()]
48+
else:
49+
# Default origins for local development and known production/dev domains
50+
origins = [
51+
"http://localhost:3000",
52+
"http://localhost:5173",
53+
"https://cert.pseudo-lab.com",
54+
"https://dev-cert.pseudolab-devfactory.com",
55+
]
56+
4557
app.add_middleware(
4658
CORSMiddleware,
47-
allow_origins=["*"],
59+
allow_origins=origins,
4860
allow_credentials=True,
4961
allow_methods=["*"],
5062
allow_headers=["*"],

getcloser/backend/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# DATABASE_URL=postgresql+psycopg2://user:password@db:5432/app_db
2+
3+
# 보안을 위해 무작위 문자열을 생성하여 설정하세요.
4+
# 예: openssl rand -hex 32
5+
SECRET_KEY=your-super-secret-key-here
6+
7+
# ACCESS_TOKEN_EXPIRE_MINUTES=60

getcloser/backend/app/core/config.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import os
2+
from pydantic import field_validator
23
from pydantic_settings import BaseSettings
34

45
class Settings(BaseSettings):
6+
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "dev")
57
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql+psycopg2://user:password@db:5432/app_db")
8+
69
"""
710
JWT 안쓸 것 같아 일단 주석 처리하고 추후 확정 시 삭제
811
"""
9-
SECRET_KEY: str = os.getenv("SECRET_KEY", "change-me-in-prod")
12+
# Secret key for JWT signing. Must be overridden in production using environment variables.
13+
DEFAULT_SECRET_KEY = "default-secret-key-change-it"
14+
SECRET_KEY: str = os.getenv("SECRET_KEY", DEFAULT_SECRET_KEY)
15+
16+
@field_validator("SECRET_KEY")
17+
@classmethod
18+
def check_secret_key(cls, v, info):
19+
"""
20+
Validate that SECRET_KEY is not using the default placeholder value in production.
21+
"""
22+
env = os.getenv("ENVIRONMENT", "dev").lower()
23+
if env in ["prod", "production"] and v == cls.DEFAULT_SECRET_KEY:
24+
raise ValueError("SECRET_KEY must be a unique, non-default value in production environments.")
25+
return v
26+
1027
ALGORITHM: str = "HS256"
1128
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
1229

platform/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ APP_HOST=your-domain.com
33

44
# Database Setting
55
DATABASE_URL=postgresql://user:pass@devfactory-postgres:5432/dbname
6+
7+
# Logging Setting
8+
ACCESS_LOGGING_IP_SALT=your-secret-salt-here

platform/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ services:
3535
restart: unless-stopped
3636
environment:
3737
- DATABASE_URL=${DATABASE_URL}
38+
- ACCESS_LOGGING_IP_SALT=${ACCESS_LOGGING_IP_SALT}
3839
- PORT=3000
3940
networks:
4041
- traefik

platform/server/src/index.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require('dotenv').config();
22
const express = require('express');
3+
const crypto = require('crypto');
34
const { Pool } = require('pg');
45
const cors = require('cors');
56

@@ -25,6 +26,35 @@ pool.query('SELECT NOW()', (err, res) => {
2526
});
2627

2728
// API Routes
29+
30+
/**
31+
* Extracts the client IP address from request headers or connection info.
32+
*/
33+
function getClientIp(req) {
34+
// Check X-Forwarded-For header (common for reverse proxies)
35+
const forwardedFor = req.headers['x-forwarded-for'];
36+
if (forwardedFor) {
37+
// Can be a comma-separated list; the first one is the original client
38+
return forwardedFor.split(',')[0].trim();
39+
}
40+
41+
// Check X-Real-IP header
42+
const realIp = req.headers['x-real-ip'];
43+
if (realIp) {
44+
return realIp;
45+
}
46+
47+
// Fallback to Express req.ip or socket address
48+
return req.ip || req.socket.remoteAddress;
49+
}
50+
51+
/**
52+
* Hashes the IP address with a salt, matching the behavior in the cert system.
53+
*/
54+
function hashIp(ip, salt = '') {
55+
if (!ip) return null;
56+
return crypto.createHash('sha256').update(salt + ip).digest('hex');
57+
}
2858
app.get('/api/health', (req, res) => {
2959
res.json({ status: 'ok' });
3060
});
@@ -33,12 +63,15 @@ app.get('/api/health', (req, res) => {
3363
app.post('/api/stats/visit', async (req, res) => {
3464
try {
3565
const { path, userAgent } = req.body;
36-
// 기존 로그 포맷에 맞춰 method는 'PAGEVIEW'로, referrer는 현재 호스트로 기록
3766
const referrer = req.headers.referer || '';
3867

68+
// Extract client IP and generate hash
69+
const clientIp = getClientIp(req);
70+
const ipHash = hashIp(clientIp, process.env.ACCESS_LOGGING_IP_SALT || '');
71+
3972
await pool.query(
40-
'INSERT INTO logging.access_log (path, method, status, user_agent, referrer, ts) VALUES ($1, $2, $3, $4, $5, NOW())',
41-
[path || '/', 'PAGEVIEW', 200, userAgent, referrer]
73+
'INSERT INTO logging.access_log (path, method, status, ip_hash, user_agent, referrer, ts) VALUES ($1, $2, $3, $4, $5, $6, NOW())',
74+
[path || '/', 'PAGEVIEW', 200, ipHash, userAgent, referrer]
4275
);
4376
res.status(201).json({ message: 'Visit logged successfully' });
4477
} catch (err) {

0 commit comments

Comments
 (0)