HIE 교환시스템 서비스 프로젝트 병동/200OK

verve·2025년 6월 15일
0
post-thumbnail

Github <<

프로젝트 개요

  • 수행 기간: 2025.05 ~ 2025.06

기술 스택:

Frontend: React
Backend: Flask
Database: MySQL
인증: Keycloak(OpenID Connect)
인프라: ESXi 기반 가상화, pfSense(방화벽), Security Onion(관제), WAF, ELK 스택(로그 시각화).snort(IDS)

프로젝트 배경

  • 의료기관 간 진료 데이터 연동의 중요성이 급격히 커지고 있음

  • 실제 현장에서는 병원마다 EMR, 진료확인서 등 환자 정보를 각기 관리해
    환자 이력 누락, 중복 진료, 정보 위변조, 개인정보 유출 등의 문제가 빈번하게 발생함

  • 병원을 오가며 치료받는 환자들은 진료 정보가 제때 전달되지 않아 불필요한 중복 검사·진료를 받거나, 필요한 의료 서비스를 제때 받지 못하는 문제가 있음

  • 기존 시스템은 정보 연동이 표준화되어 있지 않고,
    보안·개인정보 보호 관점에서도 취약함

  • 이러한 문제를 해결하기 위해 의료기관 간 실시간 데이터 교환, 신뢰성·보안 강화, 위변조·불법 조회 방지 등을 동시에 만족하는 보안 중심 HIE(Health Information Exchange) 시스템을
    직접 설계 및 구현하고자 프로젝트를 시작함

프로젝트 수행

개발 환경은 ESXi 가상화 기반의 Ubuntu 22.04 LTS로 구축되어 있습니다.

  • 3인이서 환경을 구축했으며. 본 프로젝트에 팀장 역할로 참가해 풀스택 개발 및 네트워크 설정, 대시보드 시각화를 맡았습니다
  • 프로젝트 내 의존성 패키지 관리는 requirements.txt를 통해 일관성 있게 관리하였고. 각 개발환경에서도 동일한 버전의 패키지가 적용될 수 있도록 신경썼습니다.
  • 가상환경은 ESXi를 통해 구축하였으며, 개발 및 테스트 단말은 Ubuntu Desktop 환경을 기반으로 구현하였습니다. 추후에는 사용자 접근성을 높이기 위해 실제 현업에서 많이 사용하는 Windows 환경도 지원할 계획입니다

프로젝트 전체 네트워크 및 보안 인프라 아키텍처

본 프로젝트는 내부망(사설 네트워크) 기반 환경에서만 모든 시스템이 동작하도록 설계되었습니다.
각 병원 단말, 서버, 보안 장비들은 모두 사설 IP(10.x.x.x) 대역 내에 위치하며,
외부망(인터넷)에서는 직접적으로 시스템에 접근할 수 없습니다.

병원 간 데이터 연동은 VPN(IPSec) 보안 터널을 통해서만 허용되며,병원 A와 B는 직접적으로 서로의 시스템이나 DB에 접근하거나 정보를 주고받지 않습니다. 모든 데이터 교환은 중앙 인증 및 중계 서버(HIE)를 통해 안전하게 중계·검증된 데이터만 전달되도록 설계하였고, 양 병원 간 직접 연동·직접 접근은 아예 차단되어 있습니다. 이렇게 함으로써 각 병원의 독립성과 데이터 보호, 불필요한 정보 유출 위험을 최소화했습니다.

내부망 구간에는 pfSense 방화벽, IPS/IDS, WAF, Security Onion 등의 보안 장비를 통해
외부 침입·비인가 접근·내부 이상행위까지 다단계로 탐지 및 차단하도록 구성하였습니다.

이렇게 함으로써 환자 정보 및 의료 데이터의 안전성,
실제 의료기관 보안 운영 환경과 유사한 테스트 인프라를 구현하였습니다.

모든 정보는 가상의 대학 병원 A와 B가 있다고 가정해 설계된 정보이며. 설정되는 기관의 주소는 모두 가상의 주소입니다.


로그인 페이지 인증 로그인 / 일반 로그인으로 분기를 나눔

인증 로그인시 (keyclock 연동) 되어 키클락 이동 됨

가입 인증 시 이메일. 유저 이름. 패스워드 검증을 통하고. 병원명과 의사명을 필수 필드로 받음
이메일 인증을 거치고. 2차적으로 키클락 내부 관리자 UI에서 가입 승인을 받음.

정상 로그인 시. OTP 코드를 추가로 인증함 . (구글 Authenticator연동)

메인페이지. 키클락에서 인증정보 * 토큰을 받아와 인증사용자 메뉴. 로그아웃 버튼 구현.
로그아웃시 리프레쉬 토큰을 통해 반환/폐기하며. 키클락 세션을 만료시킴.

인증 사용자 메뉴 구성은 다음과 같습니다. 직관적인 UI를 제공


프로필 확인을 눌렀을때 나오는 ui .
아이디는 uuid 값을 자동으로 생성해 저장하고. 사용자 계정의 고유성과 보안을 보장하기 위해 무작위 고유값을 출력. 사용자 실명.이메일과 별개로 중복/충돌이 없도록 설계함.

진료서를 작성하고. 빠른 도구를 통해 환자번호 자동 생성. 성별. 기한을 자동 선택할 수 있음
주민번호 입력시 고정된 양식으로 포맷을 변환함. 진료과 / 주소 모두 자동완성을 지원함.

진료 확인서는 프론트엔드(React)에서 사용자의 발급 요청이 발생하면, 이를 백엔드(Flask) API가 받아 사전 인증 및 입력 데이터 검증을 수행합니다. 이후 내부망에 위치한 HIE 서버로 요청이 전달되고, 최종적으로 데이터베이스에서 진료 기록을 조회·발급 처리합니다. 또한 주민등록번호는 AES256 암호화를 통해 암호화 되어 DB에 저장됩니다.



진료기록조회에서는 위와 같이 이름 / 진단코드. 상세기록등이 마스킹되어 표시되며. 2차 인증을 통해 해제할 수 있습니다. MFA 인증은 OTP가 적용되었습니다. 타병원기록 조회도 마찬가지의 인증을 거칩니다.


인증 이후에는 일정한 세션시간을 부여하며. 10분 동안 추가적인 OTP 인증 없이 타병원 조회 및 환자 마스킹 해제가 가능합니다.


인쇄 기능도 제공됩니다

인증된 관리자 로그인 페이지입니다. 권한을 분리시켜 관리자의 경우 관리자 로그 조회만 가능하게 설정되어있으며. 메인페이지에서 관리자 로그인시에만 노출되는 관리자 로그조회 버튼을 통해 이용 가능합니다

아래와 같은 로그들이 모두 남으며. 어떤 결과를 조회했는지. 어떤 필드를 해제했는지 조회 가능합니다.


PFsense snort 룰 설정

WAN 포트 설정

(vlan30번 대역 )

VLAN30/40/50에 OWASP Top 10, ET community, 자체 실습용 snort custom rule을 적용하여
각 구간별로 XSS, SQLi, DB direct access, brute-force, DDoS 시도를 실시간 탐지/차단하도록 구성해두었습니다.

40번대 대역 커스텀 룰

주요 서버 포트(8000, 8080, 22 등) 접근 시도,
SQLi, XSS 등 주요 공격 시그니처,
SSH 접속 시도

vlan 50번대역 커스텀 룰

1분 내 5회 이상 SSH 연결 시도 → 알림
1분 내 10회 이상 웹 로그인 POST 시도 감지
1분에 20회 이상 토큰 발급 시도
인증 서버로 SYN 패킷 100회/분 이상 시도
Ping 100회/분 이상
인증서버에 DB 포트(3306 MySQL, 6379 Redis, 5432 Postgres) 접근 감지 (비인가 접근)


시큐리티 어니언을 통해 관리자 로그에서 확인 가능했던 정보들도 확인 가능하며. 다른 정보들도 확인 가능합니다.



키바나 대시보드를 통해 클라이언트별 syslog 생성. 로그메세지 생성 어플리케이션. 이벤트 생성 어플리케이션등 다양한 로그들을 시각화해두었습니다. 실시간으로 올라오는 패킷들을 보고 공격에 대한 감지가 가능합니다



공격 시나리오 및 방어법

모든 공격 시나리오는 가상의 취약점을 만들어 공격 지점을 생성했으며. 가상의 시나리오로 모든 코드 개선 구현을 통해 취약점은 해결되어있습니다

1. 취약한 하드코딩 정보/DB 권한 탈취

전제: 하드코딩 정보 또는 환경 변수 노출, DB 정보 탈취 시 권한 상승 공격 가능

방어법

DB 접속정보 환경변수/별도 파일 분리
클라이언트 코드(index.js 등)에 DB 주소, 포트, 계정 정보가 포함되지 않도록 백엔드 서버와 통신하도록 설계

취약한 인증 정보 개선
강력한 비밀번호 정책 사용
→ 영문, 숫자, 특수문자를 조합한 랜덤 비밀번호 사용
계정별 최소 권한 원칙 적용
→ SELECT, INSERT만 필요한 계정은 DELETE, DROP, GRANT 등의 권한 제거

SQL Injection 방지
Prepared Statement (보안 쿼리) 사용
→ 사용자 입력을 직접 쿼리에 삽입하지 않고 바인딩 처리로 SQLi 차단
입력값 검증 및 필터링
→ 입력 값에 대한 화이트리스트 적용 또는 정규식 필터링

DB 계정별 최소 권한 분리 (원본 DB 직접 접근 차단)

네트워크/방화벽 정책으로 내부 접근만 허용

중요 데이터 암호화 저장(AES256 등)

모든 접근/조회 로그 남기고 관리자 승인 구조 적용


2. MGT 탈취 후 span 환경에서 HTTP 평문 스니핑

공격 개요
내부 관리 서버(MGT) 탈취 후, SPAN(Port Mirroring) 또는 Promiscuous Mode 환경을 활용하여 같은 네트워크 대역의 HTTP 트래픽을 도청하고, 로그인 정보, 쿠키 등의 민감 데이터를 탈취하는 공격, 이때 . https 통신이 아닌 http 통신을 한다는 가정. 실제 구조는 TLS 1.3 암호화가 적용되어 있음.

방어법

모든 통신 HTTPS 적용
Telnet/FTP/SNMPv1 등 평문 서비스 차단
TLS/SFTP, FTPS 등 보안 프로토콜만 사용
TLS, SFTP, SNMPv3 등 암호화된 서비스만 사용
이중 인증(2FA) 적용하여 평문 정보만으로 로그인 불가 처리
내부 트래픽이라도 SSL/TLS 전면 적용 정책 수립

공격 시나리오 3. SSH 사전 공격 웹 쉘 권한 획득

공격 전제

타깃 탐색
내부망(10.10.30.2)에서 nmap으로 SSH(22번 포트) 오픈 확인
계정/패스워드 브루트포스
hydra로 사용자 리스트(users.txt) + 패스워드 (rockyou.txt) 조합 사용
약한 인증(web/1111)으로 SSH 로그인 성공
내부 파일 탐색
web 계정으로 SSH 접속 후 .env 파일 확인 및 탈취
민감 정보 확보
Keycloak 정보, DB 계정, 서비스 Secret Key 등 포함된 .env 내용 수집
수평 이동 및 권한 상승
확보한 정보로 DB/Keycloak 등 타 시스템 침투 및 운영자 권한 탈취


방어법

1. 계정/인증 강화
SSH 키 인증만 허용, 패스워드 인증 차단
강력한 비밀번호 정책 적용
기본/불필요 계정 삭제
fail2ban으로 로그인 시도 제한

2. 네트워크/접근 제어
SSH 접근 IP 제한 (pfSense 방화벽) 및 필요시에만 한시적 허용
내부/외부 접근 정책 분리
망분리 및 포트포워딩 제한

3. 관제/탐지 시스템 운영 Security Onion으로 SSH brute-force 탐지
Security Onion으로 SSH brute-force 탐지
비정상 로그인/신규 접근 실시간 알림

4. 서버 접근 제어 솔루션 사용 

5. 파일 및 정보 보호
.env 파일 권한 제한 (chmod 600)
Secret은 Vault/Secrets Manager 등 외부 저장소 사용
.env 파일 웹 접근 차단 (.htaccess 등 적용)

6. 시스템 운영 안정성
정기적 패치 및 접근 이력 점검
침해 발생 시 계정/비번/키 전면 교체

기타 코드 취약점 개선

기존 코드

def get_conn():
    return pymysql.connect(host=DB_HOST, port=DB_PORT, ...)

# 사용할 때마다
conn = get_conn()
# 쿼리 수행 후 명시적으로 conn.close() 필요

함수 호출마다 매번 새 DB 연결 생성 → 커넥션이 많아져 메모리/리소스 낭비

import threading
from contextlib import contextmanager

class DatabaseManager:
    _instance = None
    _lock = threading.Lock()

    @contextmanager
    def get_connection(self):
        conn = None
        try:
            conn = pymysql.connect(host=DB_HOST, port=DB_PORT, ...)
            yield conn
        except Exception as e:
            if conn:
                conn.rollback()
            raise
        finally:
            if conn:
                conn.close()

db_manager = DatabaseManager()
with db_manager.get_connection() as conn:
    # 쿼리 수행 (리소스 자동 정리) 

기존에는 매번 새 DB 연결을 생성하다 보니 커넥션 누수, 리소스 낭비, 예외 시 미정리 등 다양한 문제가 있었음.
개선된 구조에서는 contextmanager와 with문을 활용해 연결-사용-정리 과정을 자동화하고,
예외 상황에서도 커넥션을 안전하게 close함으로써 전체 리소스 효율성과 안정성을 높임.
그 결과, 불필요한 커넥션 낭비 및 대기시간이 크게 감소하고, 코드 품질도 향상시킴.


3. Rate Limiting 추가

Before

# Rate limiting 없음
@app.route('/api/login', methods=['POST'])
def login():
    # 로그인 처리 코드

모든 로그인 시도에 대한 횟수 제한이 없어 무차별 대입(Brute Force), 자동화 공격에 노출되어 있음
반복적인 시도로 서버 부하 및 보안 위협 가능

After

limiter = Limiter(
    key_func=get_remote_address,
    app=app,
    default_limits=["1000 per day", "100 per hour"]
)

@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")  # 로그인 시도 제한
def login():
    # 로그인 처리 코드
  • @limiter.limit("5 per minute")로 로그인 시도 자체를 제한
  • 서버 과부하 및 계정 탈취 공격 방지

효과

1.로그인 관련 무차별 대입 공격 차단
2.비정상 트래픽/봇 요청 방지
3. 실사용자 서비스 품질 유지, 보안성 대폭 강화


4. 세션 보안 강화

Before

app.secret_key = os.environ.get('SECRET_KEY', '1234')
# 세션 보안 관련 설정 없음

기본값 사용, 쿠키 노출/세션 탈취(XSS, CSRF) 등에 취약

After

app.config.update(
    SESSION_COOKIE_SECURE=True,     # HTTPS에서만 쿠키 전송
    SESSION_COOKIE_HTTPONLY=True,   # JS 접근 금지(XSS 차단)
    SESSION_COOKIE_SAMESITE='Lax',  # 외부 도메인 전송 제한(CSRF 차단)
    PERMANENT_SESSION_LIFETIME=timedelta(hours=8)  # 세션 만료 설정
)

각종 쿠키/세션 보안 옵션 활성화, 강제 만료시간 설정

효과

쿠키 탈취·세션 하이재킹 방지
인증 정보 보안, 서비스 신뢰성 향상


5. 에러 처리 개선

Before

try:
    # 코드
except Exception as e:
    return jsonify({'result': 'fail', 'msg': str(e)}), 500
  • 모든 에러를 한 가지 방식으로만 처리
  • 원인 불명확, 사용자 안내 미흡

After

try:
    # 코드
except requests.exceptions.Timeout:
    return {'result': 'fail', 'msg': 'HIE 서버 응답 시간 초과'}, 504
except requests.exceptions.ConnectionError:
    return {'result': 'fail', 'msg': 'HIE 서버 연결 실패'}, 502
except pymysql.Error as e:
    logger.error(f"Database error: {e}")
    return {'result': 'fail', 'msg': '데이터베이스 오류가 발생했습니다'}, 500
except Exception as e:
    logger.error(f"Unexpected error: {e}")
    return {'result': 'fail', 'msg': '서버 내부 오류'}, 500
  • 에러 종류별 맞춤 응답 및 로깅, 사용자 친화 메시지 제공

효과

  • 장애 상황별 원인 파악 용이
  • 사용자에게 정확한 상황 안내, 장애 복구 효율성 상승

6. 에러 핸들러 추가

Before

# 에러 핸들러 없음
> * 404/429 등 서버 에러에 대한 별도 처리 없이 기본 에러 페이지 노출

After

@app.errorhandler(404)
def not_found(error):
    return jsonify({'result': 'fail', 'msg': '요청한 리소스를 찾을 수 없습니다'}), 404

@app.errorhandler(429)
def ratelimit_handler(e):
    return jsonify({'result': 'fail', 'msg': '요청 한도를 초과했습니다'}), 429
  • 에러 상황에서도 일관된 JSON 응답, 클라이언트/프론트엔드 개발 편의성 증가

효과

API 사용자, 프론트엔드 모두 일관된 에러 처리
예측 불가 상황에서의 서비스 신뢰성/관리성 증가


프로젝트를 마치며..

해당 프로젝트를 통해 단순히 동작하는 서비스 구현을 넘어 사용자/관리자 편의성을 모두 고려한 시스템, 보안 인프라 설계가 어려웠습니다. 특히. 실제 의료 데이터를 다룬다는 가정이였기에 사용자가 필요로 하는 편의요소를 넣으려면. 그에 상응하는 보안 요소또한 추가해야되는 부분이 어려움을 느꼈던 부분중에 하나였던것같습니다.
인증/권한 분리와 데이터 마스킹, 2차 인증같은 기능들을 넣어야하는 민감한 의료정보들이 실 구현에 어려움을 느꼈던 요소중에 하나입니다.
또, 추가적으로 구현해보고 싶은 요소가 있다면
alert 로그가 탐지되면 ai 기반으로 자동 감지 시스템이나. 차단하는 시스템을 추가적으로 구현해볼 수 있을 것 같습니다.
사소한 UI를 개선하고. 보안적 요소를 더 추가하면 실 사용에도 문제가 없을것 같다는 생각을 하게 됐던 프로젝트라고 생각합니다.

profile
보안과 개발 그 어디사이에

0개의 댓글