Spring Security 2.0.4 에 CSRF 기능이 없어 세션 동기화 토큰(Synchronizer Token)을
자체 구현. 웹취약점 점검(ZAP)의 "Absence of Anti-CSRF Tokens" 및 정적 분석에서
관리자 상태변경 요청 다수가 무방비였던 것을 조치.
- CsrfTokenInterceptor(신규): /mnt/ 상태변경 요청(POST/PUT/DELETE)에서 토큰 검증.
이중 방어 — 토큰 없거나 불일치해도 '같은 출처'면 통과(정상 화면 회귀 방지),
'교차 출처'만 차단(CSRF 공격 차단). 토큰은 SecureRandom 48자 hex.
- egov-com-servlet.xml: 인터셉터 리스트 등록 + bean 정의(enforce=true, /mnt/ 한정).
- mnt/include/head.jsp: 토큰 meta + 전역 스크립트로 모든 폼(hidden)·AJAX(X-CSRF-TOKEN
헤더)에 자동 주입. 167개 관리자 화면 공통 적용, 개별 화면 수정 없음.
검증: cross-origin+토큰없음 → 403 차단, same-origin(토큰유무 무관) → 통과(회귀 없음).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
웹취약점 점검(SER-004)에서 확인된 휴면계정 관련 취약점 2건 조치.
1) 인증 없는 휴면계정 해제 (CWE-306, 긴급)
- actionDiapause.do 가 요청 파라미터 mber_id 만으로 즉시 deleteDiapause 실행 →
아이디만 알면 제3자가 임의 계정의 휴면을 해제할 수 있었음(로컬 PoC 실증).
- 파라미터를 신뢰하지 않고, ID/PW 인증 성공 시에만 세션에 적재되는 diapauseVO 의
mber_id 로만 해제. 세션 부재 시 resCode=002 거부, 성공 시 1회성 소거.
- 정상 경로 releaseDiapauseAjax(토큰 검증)와 동일 원칙. 정상 사용자는 원래도
세션값(${sessionScope.diapauseVO.mber_id})을 보냈으므로 결과 동일(회귀 없음).
- mbl/EgovDiapause.jsp: resCode 분기로 거부 시 재로그인 안내.
2) 예측 가능한 난수 (CWE-330, 중)
- 휴면 활성화 메일 토큰(20자) 생성을 java.util.Random → SecureRandom 으로 교체.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
· 테스트 발송 버튼 — [계정 관리 정책] 화면의 관리자 수신 이메일 옆
실제 알림은 월 1회 배치에서만 나가서, 수신 주소를 바꾼 관리자가 그것이
맞는지 한 달을 기다려야 확인할 수 있었다. 같은 발송 경로(수신주소 조회 →
메일 모듈 적재·발송)를 그대로 타는 테스트를 눌러 즉시 결과를 본다.
경로가 다르면 테스트가 통과해도 실제 알림은 실패할 수 있으므로 반드시
같은 경로여야 한다.
- 성공·실패 모두 서버가 준 사유를 그대로 화면에 띄운다.
"실패했습니다"만 뜨면 주소 미설정인지 메일서버 문제인지 구분할 수 없다.
- 메일 모듈이 예외 없이 false 를 반환하는 경우도 실패로 잡는다
(insertSndngMail 은 적재 후 곧바로 전송하고 실패 시 결과코드를 'F' 로 갱신한다.
예외가 없다고 발송된 것이 아니다).
- 상태를 바꾸지는 않지만 외부로 메일이 나가므로 POST·동일출처만 받는다.
· 발주기관 확인 항목.txt 신규
제안요청서·제안서만으로 결정할 수 없어 발주기관 확인이 필요한 항목을
근거 문구·현재 구현 상태·미확정 시 영향과 함께 정리했다. A(개발 완료 조건) /
B(운영정보 필요) / C(문서에 명시 없음) / D(사업관리)로 나눴고, 확인 없이
진행한 값은 근거를 명시해 [참고] 절에 따로 적었다.
검증: mvn compile EXIT=0, JSP Jasper 오류 0건, 로컬 반영·재기동 정상
(빈 생성 오류 0건), 정책화면 302→로그인.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요약서 slide15 "로그 조회 및 감사 기능"의 구성요소 중 "관리자 알림".
종전에는 무결성 위반을 에러 로그로만 남겨서, 로그를 들여다보지 않으면
아무도 모르는 상태였다 — 탐지해 놓고 알리지 않으면 탐지의 의미가 없다.
· PrivacyAdminAlertService + Impl 신설
- 무결성 위반 탐지 시(해시체인 검증 배치)
- 이상행위 탐지 시(정기점검 배치, 0건인 달은 보내지 않음)
매달 "0건" 메일이 오면 곧 아무도 열지 않는다.
· 채널은 메일 — 다만 문서 근거는 없다
RFP·제안서 어디에도 알림 채널이 명시돼 있지 않다("관리자 알림" 네 글자뿐).
ACCOUNT_MNG_POLICY.ADMIN_EMAIL 이 이미 있고 휴면 사전알림이 그 주소로
메일을 보내고 있어 같은 경로를 재사용했다. 운영에서 메일 장애를 한 곳에서
확인할 수 있어야 한다는 이유도 있다. SMS 는 문자발송모듈 연동규격이
미확정이라(SFR-003 과 같은 블로커) 붙일 수 없다.
→ 채널 확정은 발주기관 협의 항목으로 남긴다.
· 메일에 개인정보 본문을 담지 않는다
건수·기간·취급자 계정ID·요약만 넣고 상세는 화면에서 보게 한다.
메일함이 개인정보 저장소가 되면 안 된다.
· 배치를 막지 않는다
발송 실패·수신주소 미설정 모두 로그만 남기고 배치는 계속한다. 다만 미설정은
WARN 으로 구분해 남긴다 — "알림이 안 왔다"와 "보낼 주소가 없다"는 다른 문제다.
검증: mvn compile EXIT=0, 로컬 반영 후 재기동 정상(빈 생성 오류 0건 —
privacyAdminAlertService/sndngMailRegistService/accountMngDAO 배선 확인),
frt/main.do 200. 실제 발송은 월 1회 배치라 이번에 실행하지 못했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요약서 slide15 현행진단이 "보관 기간 — 2년 이상, 기본값 730일,
관리자 설정값 미존재 → 신규 구축"으로 지목한 항목이다. 파기 배치가
자바 상수 ACCESS_LOG_RETENTION_DAYS=730 을 써서 관리자가 바꿀 수 없었다.
· ACCOUNT_MNG_POLICY.ACCESS_LOG_KEEP_DAY 신설(기본 730)
이 표에 이미 LOGIN_HIST_KEEP_DAY 가 있어 사실상 보관·파기 정책의 단일
창구다. 표를 새로 만들면 관리자가 보관기간을 두 화면에서 나눠 보게 된다.
· 법정 하한 730일을 세 겹으로 강제
화면 JS / 서버 validatePolicy / 파기 배치. 화면만 막으면 요청을 직접
만드는 경로가, 저장만 막으면 정책 행을 직접 고치는 경로가 남는다.
배치는 정책값이 730 미만이어도 730일치를 남긴다 — 한 번 지운 접속기록은
되돌릴 수 없어 설정 실수로 법정 보관기간을 깨는 쪽보다 더 남기는 쪽으로 기운다.
DB CHECK 를 쓰지 않은 이유는 MariaDB/MySQL 5.7 에서 동작이 갈려 조용히
통과할 수 있어서다.
· 접속기록 조회 화면의 "보관기간 N일" 안내도 정책값을 읽는다
파기 배치가 실제로 쓰는 값과 달라지면 안내가 거짓말이 된다.
정책 조회 실패 시 양쪽 모두 기본값으로 물러난다(안내 때문에 목록이 막히면 안 된다).
검증: mvn compile EXIT=0, JSP Jasper 오류 0건, DDL 적용 후
ACCESS_LOG_KEEP_DAY=730 확인, 재기동 정상, 정책화면·접속기록 302→로그인.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
목록은 페이지의 모든 행을 복호화하는데, 공용 MemberNmDecryptSupport 는
실패를 숨기지 않도록 예외를 던진다. 그 계약을 목록에 그대로 쓰면 암호문
한 건만 어긋나도 화면 전체가 500 이 된다 — 관리자 화면이 안 열리면 정작
무엇이 잘못됐는지 확인할 방법도 사라진다.
· 목록(MberCryptoAdminDAO.selectMberList)만 행 단위로 감싼다.
실패한 행은 이름 자리에 "(복호화 실패)" 표식과 decryptFailed=true 를 남기고,
나머지 행은 정상 표시한다. 실패를 감추는 게 아니라 어느 행인지 드러낸다.
· 단건 조회(selectMber)는 종전대로 예외를 올린다 — 상세는 값이 정확해야 한다.
다만 컨트롤러가 받아 스택트레이스 대신 원인을 문장으로 안내하고 목록으로 되돌린다.
· 실패 표식은 마스킹 대상에서 제외한다(마스킹하면 표식이 뭉개진다).
· 화면에 실패 행을 붉게 표시하고 안내 문구를 추가했다.
이 처리는 로컬 더미 DB(백필 키 분실)에서 화면을 열기 위한 임시방편이 아니라,
운영에서 키를 교체하거나 암호문 한 건이 손상돼도 관리자 화면이 살아 있어야
한다는 요구에서 나온 것이다.
검증: mvn compile EXIT=0, JSP Jasper 오류 0건, 로컬 반영·재기동 후
mberCrypto/listView.do 302→로그인, frt/main.do 200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요청서 과업내용 "다"가 요구하는 화면은 하나다 —
"ㅇ 로그 조회 및 관리 기능 구현 / - 관리자용 로그 조회 화면 개발".
제안요약서 시안도 "접속기록 조회 및 감사" 한 장만 그려져 있다.
8·9차에서 이 영역을 여러 화면으로 벌려 놓은 것을 되돌린다.
· 삭제 — 정기 점검 리포트 화면 / 이상행위 감사 화면 / 개인정보 처리기록 화면
AbnormalAccessController, abnormal/list.jsp, inspection/list.jsp 제거.
InspectionResultController 는 화면 엔드포인트(listView/saveResultView)만 걷어내고
8차 이전의 JSON 계약으로 되돌렸다.
PersonalDataAccessLogSearchVO.excludeSelect 와 그 sqlmap 조건(Mysql/Oracle 각 3곳)도
함께 제거 — processLog 진입점이 사라져 죽은 조건이 된다.
· 기능은 남긴다 (RFP 가 '기능'으로 명시한 항목이라 지우면 요건 미충족)
· "처리기록 검색·조회 기능 제공" → 처리구분 필터(조회/등록/수정/삭제 토글)
· "비정상 접근 탐지 및 기록 기능 구현" → 목록 '감사' 칸(이상/점검) 상시 표시
· SFR-004 "접속기록 정기점검을 지원" → 월 1회 배치 + PDAL_INSPECTION_REPORT 적재
· 파기이력(SER-002-2) → 접속기록 화면의 "파기이력" 버튼
AbnormalAccessDetectionService 6클래스는 감사 칸이 쓰므로 그대로 둔다.
· 메뉴 — 권한관리 하위 "접속기록 조회 및 감사"(9100044, ORDR 10) 하나로 수렴
9100048/49/50/52/53 제거. install_mysql.sql PART 3 은 [7]+[8] 을 최종형 [7] 하나로
대체했다(이미 중간형을 적용한 DB 를 위한 정리구문 포함, 신규 설치는 무영향).
검증: mvn compile EXIT=0, JSP 3종 Jasper 오류 0건, 로컬 반영·재기동 정상.
살아있는 화면 accessLog/purgeHistory/mberCrypto 302→로그인,
삭제한 inspection/abnormal/processLog **404**(제거 증명), frt/main.do 200,
통합본 전체 재실행 오류 0건(멱등).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
제안요약서 slide 14~15 의 화면 시안을 열어보니 8차 구현과 어긋나 있었다.
시안은 좌측메뉴를 "접속기록·감사" 한 묶음으로 그리고 그 아래 4개를 두며,
이상행위를 별도 화면이 아니라 접속기록 목록의 "감사" 칸에 인라인으로 표시한다.
8차는 이 넷을 권한관리 하위에 낱개로 붙여 놓았다 — 기능은 같은데 배치가 달랐다.
· 메뉴 재편 — "접속기록 · 감사"(9100052) 그룹 신설
① 접속기록 조회(9100044) ② 개인정보 처리기록(9100053, 신설)
③ 이상행위 감사(9100049) ④ 정기 점검 리포트(9100048)
파기이력(9100050)은 시안에 메뉴가 없어 내렸다. 화면은 그대로 두고
정기 점검 리포트에서 버튼으로 연다(양방향 링크). 기능을 지운 게 아니라
노출 위치만 시안에 맞춘 것이다.
그룹 메뉴의 PROGRM_FILE_NM 은 NOT NULL 이고 기존 그룹이 모두 센티넬 'dir'
을 쓴다 — NULL 로 넣었다가 ERROR 1048 로 실패한 뒤 형제 행을 실측해 정정.
· 접속기록 조회 화면에 "감사" 칸 신설
이상(처리결과 F=차단·비인가)은 붉게, 점검(정상이나 대량조회·계정공유의심·
인증실패반복 대상 또는 업무시간 밖)은 노랗게 표시하고 해당 행을 강조한다.
요약 줄도 시안대로 "검색결과 N건 · 보관기간 730일 · 이상행위 N건 탐지".
판정값(auditFlag)은 저장하지 않는다 — 탐지 임계치는 정책값이라 적재해 두면
임계치를 조정한 뒤에도 과거 기준으로 굳은 값이 남는다.
감사 표시는 집계 5회라 기간 미지정 시 최근 30일로 잘라 계산하고,
실패해도 목록은 그대로 보여준다(PSR-001).
· ② 개인정보 처리기록 — 조회(S)를 뺀 등록·수정·삭제만
화면·검색조건은 ①과 공유하고 excludeSelect 조건 하나만 다르다.
같은 표를 두 벌 관리하지 않으려는 선택이다.
검증: mvn compile EXIT=0, JSP 4종 Jasper 오류 0건, 로컬 ROOT 반영 후 재기동
정상(25.4초), 신규 processLog 포함 5개 엔드포인트 302→로그인 / 대조군 404,
통합본 privacy.install_mysql.sql PART3 [8] 병합 후 전체 재실행 오류 0건(멱등).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
개인정보보호 기능개선 8차 — 관리자 화면 4종 신설
제안서·RFP 대비 화면이 없어 증빙을 낼 수 없던 항목을 채운다.
· 정기점검 리포트(SFR-004-7 #48)
InspectionResultController 가 JSON 엔드포인트만 갖고 있어 점검결과를
화면으로 입력·보관할 수 없었다. listView/saveResultView 를 추가하고
기존 JSON 계약은 그대로 병존시킨다. 배치 집계값은 화면에서 수정하지
않는다 — 감사 증빙이라 사후 변경되면 안 된다.
· 비정상접근 탐지(과업내용 다-① #49)
탐지 5유형 로직은 있었으나 호출처가 배치뿐이었다. AbnormalAccessResultVO
에 의심 대상 행(rows)을 실어 "누가 걸렸는지"를 요약 문자열 파싱 없이
보여주고, 각 항목에서 접속기록 화면으로 조건을 넘겨 원본을 확인한다.
탐지 결과를 적재하지 않는 이유는 컨트롤러 Javadoc 참조(임계치 변경 시
과거 기준이 굳는 문제).
· 접속기록 파기이력(SER-002-2 / DBR-001-2)
PDAL_PURGE_HISTORY 는 쌓이고 있었으나 볼 수 있는 곳이 없었다.
"보관기간 경과분을 규정대로 파기했고 그 외 경로로는 손대지 않았다"를
함께 입증하는 증빙이라 조회 전용으로만 연다.
· 회원정보 암호화 관리 CRUD(추가제안 Action4 / 제안서 슬라이드16)
복호화 조회·수정·파기·신규등록 + 사유 입력 + 재인증(10분 TTL).
목록은 항상 마스킹하고 평문은 사유를 남긴 상세 한 건에서만 보인다 —
목록에서 평문이 뜨면 사유를 받는 의미가 없다.
파기는 행을 지우지 않고 식별 항목만 비운다(신청·활동 기록이 참조 중).
MBER_CRYPTO_ACCESS_HISTORY 에 처리자·일시·IP·사유·변경항목명을 남기되
개인정보 본문은 담지 않는다.
재인증에 로그인 2차인증을 재사용하지 않은 이유는 MberCryptoReauth
Javadoc 참조(세션 내내 유지돼 재확인이 안 되고, OTP 미등록자가 잠긴다).
검증: JDK8 mvn compile EXIT=0, JSP 9종 Jasper 사전컴파일 오류 0건,
로컬 ROOT 반영 후 Tomcat 재기동 정상(신규 sqlMap 4건 기동시 파싱 통과),
신규 엔드포인트 6개 302→로그인 / 대조군 404 / frt/main.do 200.
DB: MBER_CRYPTO_ACCESS_HISTORY DDL + 메뉴 9100048~9100051
(privacy.install_mysql.sql PART1·PART3 에도 병합, 재실행 안전)
후속 발견: 기존 탈퇴 구문 mberManageDAO.updateDeleteMber_S 는 개인정보를
비우면서도 이름(MBER_NM/ENC/HASH)은 지우지 않는다 — 파기 누락 소지.
신설 화면의 파기는 이름까지 파기한다. 기존 경로 보정은 별도 판단 필요.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
DBCP 는 validationQuery 가 없으면 testWhileIdle 을 켜도 아무 검사도 하지 않는다.
종전에는 validationQuery 가 주석 처리돼 있고 검사 주기가 10000000ms(약 2.8시간)
이라 사실상 무검사였다. MySQL·MariaDB 의 wait_timeout 은 기본 8시간이므로 야간처럼
오래 논 뒤 첫 요청이 서버가 이미 끊은 커넥션을 받아 실패한다.
실제 발생: 2026-08-26 AccessLogDownloadJobWorker
unexpected end of stream, read 0 bytes (socket was closed by server)
- validationQuery = SELECT 1 (종전 주석의 'select 1 from dual' 은 Oracle 표기)
- validationQueryTimeout 3초, testOnBorrow 켬
- 유휴 검사 주기 2.8시간 → 5분, 유휴 정리 기준 30분
commons-dbcp 1.3 의 BasicDataSource 에 네 setter 가 모두 존재함을 jar 로 확인했다
(없는 프로퍼티를 넣으면 컴파일은 통과하고 WAS 기동 때만 터진다).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
act 는 run: 스텝을 bash -e -o pipefail 로 실행한다. `find ... | head -1` 은
head 가 첫 줄을 받고 파이프를 닫는 순간 find 가 SIGPIPE(141)로 죽어 파이프라인
전체가 실패로 판정된다. 빌드가 BUILD SUCCESS 로 끝나고 192MB WAR 까지 만든 뒤
"산출물 검증" 스텝이 출력 한 줄 없이 죽어 배포가 멈췄다.
실측: bash -e -o pipefail 에서 `find | head -1` → 141, `-print -quit` → 0.
- 산출물 검증: find 를 -print -quit 로 바꿔 파이프 자체를 없앤다
- 사전 점검: `free -h | head -2` 도 같은 경합을 안고 있어 파이프를 제거
리포 주석이 경고하던 바로 그 함정이다(deploy/dev 배포 스크립트의
unzip -l | grep -q 사고와 동일 원인).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
관리자 메뉴 9100045(업무유형 매핑 관리)·9100046(로그인 접속기록) 두 화면을
폐기하고 관련 5계층·sqlMap·JSP를 제거했다. 접속기록은 PERSONAL_DATA_ACCESS_LOG
경로로 일원화한다.
- JobMapping 5계층(Controller/DAO/Service/Impl/Resolver/SearchVO)과
ConectLog 5계층, PersonalDataLogAspect 제거
- PrivacyJobMapping_SQL_{Mysql,Oracle}.xml 제거 + sql-map-config 등록 라인 삭제
- left.jsp의 9100045/9100046 분기와 상위 하이라이트 조건 제거
- 메뉴 폐기 DML 신설(권한매핑→메뉴→프로그램 순, 멱등) — 종전 등록 DML 2건 대체.
JSP와 반드시 동반 배포해야 URL 직접 접근 경로가 남지 않는다
- privacy.install_mysql.sql에 PART 4(뷰 GNR 컷오버) 반영, 메뉴 번호 갱신
- MemberNmCryptoController 신설 — 회원 이름 암호화 백필 트리거
- SessionPolicyInterceptor에 stampLogin/clearLoginStamp 추가
(EgovLoginController가 참조 — 이게 없으면 컴파일되지 않는다)
- pom: mysql-connector를 5.1.17로 고정(8.0.20 비활성화). 배포 환경의
WEB-INF/lib 실측값과 일치시켜 WAR 배포 시 드라이버가 바뀌는 지뢰를 제거
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
원본 MySQL 8과 동작을 맞추려고 이 모드를 켰던 것이 역효과였다. MySQL 8은
GROUP BY 컬럼에 대한 함수 종속성을 판정해 집계에 없는 컬럼도 통과시키지만
MariaDB는 그 판정을 하지 않는다. 같은 sql_mode를 그대로 옮기면 로컬에서
잘 돌던 쿼리가 서버에서만 터진다.
증상: 관리자 2차인증 로그인 직후 /mnt/main.do 가 404. 실제로는 대시보드
쿼리가 'Z.STATUS1' isn't in GROUP BY 로 실패하고 예외 처리가 404로 이어진 것.
ONLY_FULL_GROUP_BY 제외는 MariaDB의 기본값이기도 하다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
메인 화면 하나가 woff 폰트만 3MB 넘게 끌어오는데 압축도 캐시 헤더도 없었다.
유선망에서는 티가 안 나지만(같은 폰트 0.15초) 테더링에서는 25초가 걸려
브라우저가 로딩을 끝내지 못하고 "접속 안 됨"처럼 보인다.
- mod_deflate: HTML 52,665B → 9,516B (82% 감소)
- 폰트 1년 immutable, 이미지 7일, css/js 1시간 캐시 — 재방문 시 3MB 재다운로드 제거
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
웹서버(175.118.124.176)의 Gitea 러너가 JDK8 로 WAR 을 빌드해 WAS(27.255.76.2)의
Tomcat·MariaDB 컨테이너로 배포한다. WAS 는 웹서버를 통해서만 닿고, Tomcat 은
127.0.0.1 바인딩이라 웹서버가 SSH 터널로 잇는다.
- WAS 는 가용 메모리가 3G 안팎(llama-server 200G+ 점유)이라 빌드를 웹서버에서 한다
- claudeops 가 sudo 를 쓸 수 없어 Tomcat·MariaDB 를 컨테이너로 올렸다
- 배포 스크립트를 매 실행마다 저장소본으로 덮어써 서버 사본 드리프트를 막는다
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
545b49d(4차 델타)에서 EgovDtaUseStats_SQL_Mysql.xml resultMap에
userNmSe 컬럼 매핑만 추가하고 대응 Java 필드/DAO 조건부복호화 배선을
누락했다. iBatis2는 이 프로퍼티 부재를 컴파일타임이 아닌 Spring
컨텍스트 기동시 sql-map-config 전체 파싱 단계에서 검증하므로,
컴파일은 성공했지만 실제 WAS 재기동 시 ProbeException으로 컨텍스트
전체가 기동 실패해 로컬 ROOT가 다운되는 사고로 이어졌다.
- DtaUseStats.java: userNmSe 필드 + getter/setter 추가
- DtaUseStatsDAO.selectDtaUseStats(): GNR일 때만 조건부 복호화 배선
재기동 후 컨텍스트 정상 기동 확인(오류 0건), /frt/main.do 200,
/mnt/sec/accountPolicy/policyView.do 302로 검증.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
회원정보 암호화(Action4)의 마지막 단계. 545b49d로 반영한 소비자 정비(66개 표시 +
전체 검색전환)를 전제로, 두 공유 뷰의 GNR 분기를 MBER_NM(평문)에서 MBER_NM_ENC(암호문)로
전환하고 검색용 USER_NM_HASH 컬럼을 추가하는 DDL을 127.0.0.1:3306 dev DB에 실제 실행했다.
- information_schema.views.VIEW_DEFINITION을 서버사이드 REPLACE()로 최소 치환(PREPARE/
EXECUTE) — comvnuserinfo의 한글 리터럴(USER_GUBUN_NM)을 클라이언트에서 재입력하지
않아 터미널 인코딩 문제를 원천 회피
- 실행 전 원본 정의를 privacy_view_def_backup 테이블에 백업, 치환 건수/컬럼 존재
여부를 assertion으로 강제해 잘못된 정의 반영을 차단
- 행 수(USER_SE/USER_GUBUN별 COUNT) 실행 전후 동일 확인, GNR 표본 암호문+해시 노출
확인, USR/ENT 표본 평문 그대로 유지 확인 — 상세 로그는 _workspace/deploy/20260811_0850/
DB_APPLY_LOG.md(gitignored, 로컬 전용)
주의: 이번엔 코드(545b49d) 커밋 후 로컬 WAS 재기동 전에 DB DDL을 먼저 실행했다 —
다음 반영 시 코드부터 배포하고 재기동할 것(LAST_DEPLOYED.md 참조).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
일반회원 이름(COMTNGNRLMBER.MBER_NM) 암호화 저장(추가제안 Action4)의 마지막 단계로,
comvnusermaster/comvnuserinfo 뷰를 거쳐 이름을 표시·검색하는 전체 소비자(66개 표시
구문 + 신규 발견된 검색(LIKE/exact-match) 구문)를 뷰 재정의 이전에 전수 정비했다.
- 표시: 판별 컬럼(USER_SE 등) 추가 후 GNR일 때만 조건부 복호화
- 검색: LIKE/exact-match를 (HASH = #hash# OR 원본 조건) 패턴으로 전환
- 과정에서 발견한 기존 버그(EgovBBSLoneMaster/EgovBoard의 columnIndex 위치
불일치, BizPrvol/EgovLoginUsr/BizPrapp의 누락된 검색 전환 지점)를 최소 범위로
수정하거나(내 필드에 한해) 영향받지 않도록 우회 처리
뷰 자체(ALTER VIEW)는 아직 미실행 — 모든 소비자 정비가 끝나야 실행 가능한
all-or-nothing 게이트가 있어 별도로 진행한다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
관리자 계정이 5회 실패로 잠기면 해제 화면도 로그인이 있어야 들어갈 수 있어
아무도 풀 수 없는 상태가 될 수 있었다. 지정 계정을 LOGIN_LOCK_EXEMPT
화이트리스트로 등록하면 실패 잠금 판정만 면제되고, LOGIN_HISTORY 감사이력과
2단계 인증(OTP)은 예외 없이 그대로 적용된다. 캐시는 fail-closed로 조회
실패 시 전 계정 정상 잠금 판정을 유지한다.
접근통제 정책 화면에 '잠금 예외 계정' 섹션을 추가해 등록/해제하며,
같은 화면의 '잠금 해제 방식'은 실사용 중인 단일 옵션만 남기도록 단순화했다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
미사용계정 현황(#11, 메뉴 9100037)이 대상이 개인정보취급자(EMP) 계정임을
화면·메뉴명에서 알 수 없어 오해 소지가 있었다. "장기 미사용 계정" →
"장기 미사용 관리자 계정"으로 명시(요약표/목록 캡션, 메뉴명, 프로그램 설명).
이미 배포된 환경을 위해 재실행 안전한 UPDATE 문을 DML에 추가.
목록 행별 "말소"/"잠금" 버튼은 시각적으로 과도하게 커서 축소(.btn3.mini).
상단 "선택 말소" 일괄 버튼은 그대로 둠.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
일반회원(GNR)은 탈퇴 즉시 하드 DELETE되어 휴면 전환 기반 지연 파기 대상이
아니었으므로 배치에서 제외. 관리자(개인정보취급자) 계정은 SFR-002 핵심요건
(개인정보보호위 점검 지적사항 — 휴면 대상이 일반회원에 한정되고 개인정보
취급자가 미포함)이라 로직은 그대로 유지하고 대상만 EMP로 좁혔다.
정책 화면(계정 관리 정책) 안내 문구도 관리자 계정 대상임을 명시.
스키마 변경 없음(ACCOUNT_MNG_POLICY.DELETE_PRESERVE_DAY/PURGE_USE_YN,
ACCOUNT_DELETE_HISTORY 재사용).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5개 세션에 걸쳐 쌓인 미커밋 변경분을 한 번에 정리한다. 주 산출물은 이번 세션의
접근통제 정책 적용(SER-001 요건 4·5)이고, 나머지는 앞선 세션들의 백로그다.
■ 접근통제 정책 적용 (SER-001 요건 4·5, 이번 세션)
「개인정보보호강화 파일럿 개발항목」 접근통제 정책 적용(Action2) 시안 반영.
- ACCESS_CONTROL_POLICY(단일행) + 관리자 화면 신규 (메뉴 9100047)
· 계정 잠금: 인증 실패 임계값(10 -> 5), 잠금 해제 방식(관리자화면+이력 / +사유필수)
· 자동 로그아웃: 미사용 시간(60 -> 30분), 만료 임박 안내 사용여부·시점
· 세션 재발급: 2차 인증 성공 시 세션 고정(Session Fixation) 차단 여부
- SessionPolicyInterceptor 신규 — /mnt/ 요청마다 정책값으로 세션 타임아웃 적용.
web.xml 재배포 없이 값이 바뀐다(2025.9 자료요청 3번 "시간 설정 기능 없음" 해소).
web.xml <session-timeout>30 은 이제 폴백값이다.
- 잠금 임계값 저장 시 인증 경로가 읽는 LOGIN_LOCK_COUNT 를 함께 동기화.
정책 테이블만 고치면 화면 숫자만 바뀌고 실제 인증은 옛 값을 계속 쓰기 때문이다.
- 관리자 상단(top.jsp): 로그인 후 경과시간 + 자동 로그아웃 잔여 카운트다운,
만료 임박 안내와 연장(/mnt/session/extend.do).
세션 상태를 주기 폴링하지 않는다 — 그 요청 자체가 세션을 갱신해
'미사용 자동 로그아웃'이 영원히 오지 않기 때문이다.
- 자동 로그아웃 시 로그인 화면 이동: actionLogout.do?expired=Y 로 세션 invalidate 후
로그인 화면 + 사유 안내. 수동 로그아웃(프론트 메인 이동)은 기존 동작 유지.
- 계정잠금관리: 정책이 '사유 필수'면 해제 사유 강제, ACCOUNT_UNLOCK_HISTORY 적재.
■ EgovLoginLogAspect fail-safe (회귀 수정)
접속기록 적재 예외가 @Before 어드바이스 밖으로 나가 로그인·로그아웃이 통째로 500 이
되고 있었다(로컬: MySQL 8 + mysql-connector 5.1.17 의 tx_isolation 조회 실패).
클래스 Javadoc 이 이미 fail-safe 를 명세하고 있었으나 구현이 따르지 않았다.
try/catch 로 인증 흐름을 살리되, 접속기록 유실은 SFR-004 직결이라 ERROR 로 남긴다.
■ 앞선 세션 백로그
- 접속기록(SFR-004): 조회·CSV 비동기 다운로드(PDAL_DOWNLOAD_JOB), 업무유형 매핑
- 로그인 접속기록(#62): COMTNLOGINLOG 접속IP·접속ID 유실 복구 + 접속위치 스냅샷
- 권한관리(SFR-001): 권한변경 이력 CSV 비동기 다운로드, 신청·승인 워크플로 화면 정리
- 미사용계정(SFR-002): 현황 화면 시안 반영(요약·역할·잠금/말소·조치 이력)
- 회원정보 암호화(추가제안 Action4): AES 암복호화·검색용 해시, 이관 배치와 사전 백업
■ 커밋 제외
font/(글꼴 설치본 22M), 제안서 문서(pptx/hwp), .claude/(하네스) 는 소스가 아니라 제외.
.claude/ 버전관리 여부는 별도 판단이 필요하다.
배포 산출물: _workspace/deploy/20260810_1300/ (DELTA.md, sql/, APPLY_LOG.md)
브라우저 검증(2단계 인증 필요)은 APPLY_LOG.md B1~B7 로 남겨둔다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
접속기록(SFR-004/#33):
- AccessLogQueue 포화 방어. 종전 offer(timeout) 무한 반복은 컨슈머 정지 시
요청 스레드가 영구 대기해 WAS 스레드 풀 고갈 → 사이트 전면 정지로 이어졌다.
대기를 3회로 제한하고 포화 차단기(saturated)를 도입해 지속 포화 시 즉시 판정한다.
- 유실은 조용히 넘기지 않는다. recordDrop 이 누적 집계 + 초기 20건/이후 100건 표본
ERROR 로그로 남긴다(법정 보관 대상이므로 사후 추적 가능해야 함).
- 컨슈머 루프가 Exception 이 아닌 Throwable 까지 흡수한다. Error 하나로 컨슈머가
죽으면 running=true 인 채 이후 전 요청이 포화 대기에 빠졌다.
- 자체 데몬 워치독(30초) 추가 — 컨슈머 사망 시 재기동, 적체 경고, 차단기 자동 해제.
task:annotation-driven 활성화 여부와 무관해야 하므로 @Scheduled 를 쓰지 않는다.
- 해시체인 순번 보장을 위한 단일 컨슈머 전제는 유지(병렬화 금지).
인증정보 보호(SER-001/SER-003/#27):
- EgovFileScrty 사용자별 랜덤 솔트(S1: 형식) 도입, matches() 로 검증 일원화 —
기존 무솔트 계정과 공존한다.
- 비밀번호 변경 시 현재 비밀번호 검증 복구. 종전에는 DB 값을 자기 자신과 비교해
항상 true 였고, 입력한 oldPassword 가 어디에도 쓰이지 않았다.
- 평문 비밀번호 println 제거.
관리자 화면: 접속기록 조회, 업무구분 매핑 관리, 권한변경 이력, 미사용계정 현황.
계정 파기(AccountDelete) 서비스 및 DDL/DML 추가.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#22(실패이력 활성 경로 이설)로 적재는 살아났으나 LOGIN_HISTORY가
ID/CREATE_DATE/GUBUN 3컬럼뿐이라 (1) 접속 IP 미기록 (2) 로그인 성공 시
당일 이력 DELETE로 무차별 대입 흔적 소멸 문제가 있었다.
- DDL: LOGIN_HISTORY에 CONECT_IP(45)/RESET_YN + 인덱스 2종,
ACCOUNT_MNG_POLICY.LOGIN_HIST_KEEP_DAY(기본 730일) 추가
- 적재: insertFailHistory 컬럼 명시 전환 + ClientIpResolver IP 세팅
(종전 무컬럼 VALUES(3값)은 컬럼 추가 시 ERROR 1136 — DDL과 한 쌍)
- 보존: 성공 시 DELETE -> RESET_YN='Y' 소프트 리셋.
selectFailCount가 RESET_YN='N'만 세므로 계정잠금 동작은 종전과 동일
- 화면: /mnt/sec/loginFailHistory/listView.do (계정·구분·IP·기간·리셋여부
검색 + 페이징), 메뉴 9100039 + left.jsp 분기
- 정책화면: 계정 관리 정책에 보관기간 입력 항목·검증 추가
- 배치: purgeExpiredLoginFailHistoryScheduler 매일 03:30, 1회 5,000건 상한
- Mysql/Oracle sqlmap 쌍 + sql-map-config 등록
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 나눔고딕 글꼴 배포 중 드러난 문제들이다.
1) build-and-upload.sh: 업로드 검증이 없어 잘린 WAR이 통과했다
pscp가 대용량 전송 중 약 30초 지점에서 조용히 끊기며 exit 0을 반환해,
194MB WAR이 66MB/84MB로 잘린 채 "업로드 완료"로 처리됐다. 그 WAR로
배포가 돌아 실패했다. 30MB 조각 전송 + 조각별 크기 확인(3회 재시도) +
서버 결합 후 size/md5 대조를 통과할 때만 arko.war 로 교체하도록 바꿨다.
2) build-and-upload.sh: mvn -ntp 는 Maven 3.6.1+ 전용
PATH에 3.5.4가 잡히면 "Unrecognized option: -ntp"로 빌드가 즉시 죽는다.
진행률 억제 옵션일 뿐이라 제거했다.
3) arko-dev-deploy.sh: WAR 검증이 파이프+pipefail 조합으로 취약
정상 WAR인데 Actions 실행 중 "WEB-INF/web.xml 없음"으로 오판해 배포가
멈췄다(서버에서 동일 명령 재실행 시 20/20 통과). unzip 목록을 한 번만
읽어 변수에 담고 파이프 없이 검사한다.
※ 서버의 /opt/arko-dev/bin/ 사본은 워크플로가 갱신하지 않으므로 별도 반영 필요.
덧붙여 워크플로 주석의 "빈 커밋으로 재배포" 안내를 정정했다 —
paths-ignore 때문에 변경 경로가 없는 커밋은 워크플로가 실행되지 않는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞선 배포(task 14)가 WAR 검증 단계에서 실패했다.
서버의 arko.war 는 로컬과 size/md5 동일함을 확인했고(203413124,
b85bc6c25e3cb245c1718e68123e4b33), 동일 검증 명령을 서버에서 재실행하면
정상 통과한다. 러너 컨텍스트 일시 실패로 판단해 재전개만 트리거한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
개별 CSS에 맑은고딕/돋움/굴림/dotum/Tahoma/Arial 지정이 흩어져 있고,
나눔 @font-face 도 파일마다 4중(nanumttf/nanumeot/_r) 으로 중복 선언되어
화면마다 글꼴이 달라 보이던 것을 하나로 정리한다.
- css/site/mnt/nanumgothic.css 신규: "NanumGothic" 패밀리 단일 정의(400/700/800),
local() 우선 + eot/woff/ttf fallback, 전역 적용 및 벤더 위젯 override
- head.jsp 에 마지막 스타일시트로 등록 → /mnt/include/head.do 를 가져가는
관리자 화면 293개(egovframework.com.* 표준화면 포함)에 일괄 적용
- mnt CSS 11개 파일의 font-family 지정 39곳 교체, 레거시 @font-face 16줄 제거
- head.do 를 쓰지 않는 쪽지 팝업 4개는 개별 link 추가
- ckeditor/contents.css: "nanumttf_r" 이 @font-face 선언 없이 사용되어
실제로는 적용되지 않던 상태를 정상화(관리자 21 / 프론트 26 화면 공용)
- NanumGothicBold.ttf 추가: 기존에 Regular/ExtraBold 만 있어
font-weight:bold 가 ExtraBold 로 렌더링되던 것을 정상 Bold 로 교정
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
러너가 작업 하나를 끝낸 뒤 폴링을 멈추는 문제가 있다. 프로세스는 살아 있고
Gitea로의 TCP 연결도 ESTABLISHED 인데 응답 없이 물려 있으며 CPU 사용량이 완전히
정지한다. nginx 경유 제거(직결 재등록)와 0.2.11->0.2.13 업그레이드로도 재현되어
근본 원인을 잡지 못했다.
arko-runner-watchdog.timer 가 1분마다 "90초 넘게 대기 중인 작업이 있는데
실행 중인 작업은 없는" 상태를 확인해 러너를 재기동한다. 실행 중인 작업이 있으면
건드리지 않으므로 배포 도중 끊길 위험은 없다.
검증: 대기 중이던 run 67을 워치독이 감지해 재기동했고 배포가 정상 완료됐다.
runner를 nginx 경유(https://git.zioinfo.co.kr)로 등록하면 declare까지는 되는데
작업을 가져오지 않고 재기동해야만 처리하는 문제가 있었다. long-poll이 nginx의
proxy_buffering(기본 on)에 물리는 것으로 보인다. Gitea가 같은 서버에 있으므로
127.0.0.1:9003 직결로 재등록해 해결했고, 재발 시 쓰도록 스크립트로 남긴다.
deploy/README.md에 전체 구조(개발 WAS/DB 단일, 운영은 웹 티어 전용),
접속 방향이 양쪽으로 다른 이유(NAT), 키 제한 범위, 알려진 이슈를 정리했다.
웹 티어(211.37.173.197 / Rocky 8):
- csv.wise.ai.kr nginx vhost + Let's Encrypt SSL
- WAS는 SSH 터널(arko-was-tunnel.service)로 연결 — 개발서버가 NAT 뒤라
8088이 포워딩되지 않고 Tomcat도 127.0.0.1 바인딩이기 때문이다.
키는 WAS에서 restrict,permitopen="127.0.0.1:8088" 로 묶어 셸 접근 불가.
- 정적 리소스는 운영 웹이 직접 서빙하고 없으면 @was 폴백 — 전환 중에도 깨지지 않는다.
파이프라인:
git push -> 개발 WAS 배포 -> 개발 헬스체크 -> 운영 웹 정적 배포 -> 운영 검증
개발 헬스체크를 통과한 뒤에만 운영으로 넘어간다.
접속 방향은 개발->운영(개발서버는 NAT 뒤라 역방향 접속이 불가능하다).
Gitea 1.22.3은 workflow_dispatch(수동 실행)와 inputs 컨텍스트를 지원하지 않아
워크플로가 Actions 목록에 표시되지 않았다. 트리거를 push 하나로 단순화한다.
롤백은 서버에서 직접 수행하도록 README에 절차를 남긴다.
Tomcat 힙도 768m -> 448m로 낮춘다. 가용 메모리 1G 안팎인 서버에서
Tomcat 재기동 직후 서버 전체가 응답 불능이 된 사례가 두 번 있었다.
- 00-server-setup.sh: JDK8(Temurin)·Tomcat9·systemd·nginx·act_runner 구축 (멱등)
- arko-dev-deploy.sh: WAR 검증→백업→ROOT 재전개→환경설정 주입→기동→헬스체크→실패 시 롤백
- build-and-upload.sh: 개발자 PC에서 JDK8 빌드 후 WAR 업로드 (서버 자원 보호)
- deploy-dev.yml: master push 시 자동 배포, 수동 재배포·롤백 지원
서버가 2코어/메모리 여유 1G에 40여 개 서비스가 동거하는 환경이라
Maven 빌드는 서버에서 돌리지 않고 개발자 PC에서 수행한다.
JSP가 리소스를 컨텍스트 경로 없이 하드코딩해 ROOT 전개만 가능하며,
globals-{dev,live}.properties는 .gitignore 대상이라 서버 config/에 두고 배포 시 주입한다.
권한 신청·승인 워크플로(N:신설=추가부여)로 사용자가 AUTHOR_CODE를 2개 이상
갖게 되면, 레거시 eGov 표준 메뉴 쿼리(selectMainMenuHead/Left/LastMenuNo(Cnt))의
"= (서브쿼리)"가 다중 행을 반환해 Subquery returns more than 1 row로 500 오류가
발생했다(2FA/OTP 등록 직후 /mnt/main.do 진입 시 재현). 서브쿼리를 IN으로,
상위 SELECT에 DISTINCT를 추가해 다중 역할을 지원하도록 수정.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>