[Specification] 보안 & 거버넌스 정책
1. RBAC 권한 체계
1.1. 역할 정의
| 역할 |
권한 |
담당 |
| Developer |
소관 사이트 배포 요청 |
파일 전송 + 재기동 |
| SM 운영자 |
로그 조회, 자원 정비, 크론 관리 |
운영 유지보수 |
| 팀장 (Owner) |
1차 배포 승인 |
소관 시스템 |
| 대리 (Deputy) |
팀장 부재 시 대리 승인 |
|
| PM |
최종 HITL 검증, 거버넌스 규칙 수정 |
전체 통제 |
| 보안책임자 |
인프라 변경/보안 작업 3차 승인 |
|
1.2. 다단계 승인 워크플로우
APPROVAL_POLICY = {
"MAINTENANCE": {"steps": 1, "approvers": ["OWNER"]},
"DEPLOY_NORMAL": {"steps": 2, "approvers": ["OWNER", "PM"]},
"DEPLOY_SECURE": {"steps": 3, "approvers": ["OWNER", "PM", "SECURITY"]},
}
def get_approval_policy(task_category: str) -> dict:
if task_category in ["LOG_ANALYSIS", "CRON_MANAGE"]:
return APPROVAL_POLICY["MAINTENANCE"]
elif task_category in ["DEPLOY", "DATA_CLEAN"]:
return APPROVAL_POLICY["DEPLOY_NORMAL"]
else: # SSL, INFRA_CHANGE
return APPROVAL_POLICY["DEPLOY_SECURE"]
2. 계층적 승인 엔진 (에스컬레이션)
def get_approver_chain(system_id: str) -> str:
"""SLA 초과 시 자동 에스컬레이션"""
info = db.execute(
"SELECT owner_id, deputy_id, supervisor_id, sla_minutes, is_active "
"FROM TB_SYSTEM_OWNER WHERE system_id = %s", (system_id,)
).fetchone()
if info["is_active"] == "Y":
return info["owner_id"] # 1순위: 담당 팀장
elif info["deputy_id"]:
return info["deputy_id"] # 2순위: 대리 승인자
else:
return info["supervisor_id"] # 3순위: 상위 관리자
def check_escalation(task_id: str):
"""SLA 초과 여부 체크 → 자동 이관"""
task = db.get_task(task_id)
owner_info = db.get_system_owner(task["system_id"])
elapsed = (datetime.now() - task["created_at"]).seconds / 60
if elapsed > owner_info["sla_minutes"] and task["status"] == "PENDING_APPROVAL":
next_approver = get_approver_chain(task["system_id"])
db.update_task_approver(task_id, next_approver, "ESCALATED")
messenger.send_urgent_notification(next_approver, f"[긴급] {task_id} 승인 대기")
audit.log_action(task_id, f"Escalated to {next_approver}")
3. 자격증명 보안
3.1. 암호화 저장 정책
- SSH/FTP 비밀번호: AES-256 암호화 → TB_SERVER_INFO.os_pw_enc
- 실행 시점에만 메모리 로드, 로그 출력 금지
- 메신저 응답창 노출 절대 금지
- 주기적 비밀번호 갱신 시 DB 암호화 업데이트
3.2. SSH 접속 보안
# opsagent 계정 sudoers 설정 (대상 서버에 1회 설정)
# /etc/sudoers.d/opsagent
opsagent ALL=(ALL) NOPASSWD: /usr/bin/kill, /usr/bin/systemctl restart *, /app/scripts/shutdown.sh, /app/scripts/startup.sh
4. 감사 로그 해시 체이닝
import hashlib, json
def compute_audit_hash(log_data: dict, prev_hash: str) -> str:
"""SHA-256 해시 체인 생성"""
content = json.dumps(log_data, ensure_ascii=False, sort_keys=True)
combined = f"{content}{prev_hash}"
return hashlib.sha256(combined.encode()).hexdigest()
def log_execution(task_id: str, server_ip: str, command: str, exit_code: int, result: str):
"""감사 로그 저장 — 이전 로그의 해시를 체인에 포함"""
prev = db.get_last_audit_log()
prev_hash = prev["result_hash"] if prev else "GENESIS"
log_data = {
"task_id": task_id,
"server_ip": server_ip,
"command": command,
"exit_code": exit_code,
"exec_time": datetime.utcnow().isoformat()
}
new_hash = compute_audit_hash(log_data, prev_hash)
db.insert_audit_log({**log_data, "result": result, "result_hash": new_hash})
def verify_audit_integrity() -> bool:
"""해시 체인 무결성 검증 — 위변조 탐지"""
logs = db.get_all_audit_logs_ordered()
prev_hash = "GENESIS"
for log in logs:
expected_hash = compute_audit_hash(
{k: v for k, v in log.items() if k != "result_hash"}, prev_hash
)
if expected_hash != log["result_hash"]:
alert_security_breach(log["log_id"])
return False
prev_hash = log["result_hash"]
return True
5. Kill-Switch (비상 정지)
@app.post("/api/emergency/kill")
async def emergency_kill(operator_id: str, reason: str):
"""메신저 '정지' 명령 → 전체 배포 프로세스 즉시 중단"""
# 진행 중인 모든 IN_PROGRESS 태스크 중단
active_tasks = db.get_tasks_by_status("IN_PROGRESS")
for task in active_tasks:
task_manager.abort(task["task_id"])
trigger_rollback(task["task_id"])
# 감사 로그
log_execution("EMERGENCY", "ALL", f"KILL_SWITCH by {operator_id}", 0, reason)
# PM에게 알림
messenger.send_to_all_pm(f"🚨 비상 정지 실행: {operator_id} — {reason}")
return {"status": "KILLED", "aborted_tasks": len(active_tasks)}
6. 보안 감사 체크리스트 (GS 인증 대응)
| 항목 |
검증 방법 |
주기 |
| 로그 무결성 |
해시 체인 전체 역추적 |
매일 00:00 |
| 계정 잠금 탐지 |
faillock 명령어 원격 스캔 |
1시간마다 |
| 설정 파일 변조 |
SHA-256 Checksum 비교 |
1시간마다 |
| 인증서 만료 |
openssl enddate 파싱 |
매일 04:00 |
| 디스크 임계치 |
df -h 자동 스캔 |
5분마다 |
| 비인가 포트 |
ss -tlnp 스캔 |
1시간마다 |
7. 데이터 무결성 보호 (배포 중 롤백)
class IntegrityShield:
def protect_rollback(self, task_id: str, executor: SSHExecutor):
"""롤백 수행 시에도 감사 기록 보존"""
# 롤백 직전 현재 해시 상태 기록
log_execution(task_id, executor.host, "ROLLBACK_START", 0, "롤백 시작")
# 실제 롤백
result = executor.execute_command("sh /app/scripts/rollback.sh")
if result["exit_code"] != 0:
# 롤백 실패 → 수동 점검 필요 상태로 변경 (다음 배포 차단)
db.update_task_status(task_id, "MANUAL_INTERVENTION_REQUIRED")
messenger.send_critical_alert(f"🔴 {task_id} 롤백 실패 — 수동 점검 필요")