Devin.KR

프로세스와 열린 연결 확인

80분 안팎

학습 목표

PID·실행 계정·리스닝 주소를 연결합니다.

개념

프로세스 존재와 연결 노출을 분리하기

앱 이름이 프로세스 목록에 보인다는 사실은 요청을 받을 수 있다는 뜻이 아닙니다. 리스너가 열렸어도 그 소켓을 만든 프로세스가 예상한 앱인지 확인해야 합니다. 이번 목표는 한 시점의 PID, 실행 계정, TCP 리스닝 주소를 연결하는 것입니다. 이를 연결하면 루트로 실행된 앱이나 의도하지 않은 전체 인터페이스 바인딩을 근거를 갖고 지적할 수 있습니다.

앞 레슨은 파일 메타데이터를 측정했습니다. 이번에는 실행 중인 프로세스를 관찰합니다. observe.cjs는 앞 모듈의 server.cjs를 불러와 잠깐 리스너를 열고, ps와 ss를 실행하고, HTTP 요청을 보낸 뒤 정상적으로 닫습니다. 프로그램이 종료하면 PID는 더 이상 같은 의미로 유지되지 않습니다. 보고서에 명령 결과와 관찰 시각을 함께 남기는 이유입니다.

PID와 실행 주체

PID는 현재 PID 네임스페이스의 프로세스 번호입니다. PPID는 부모 번호입니다. 컨테이너 내부와 호스트에서는 같은 프로세스의 번호가 다르게 보일 수 있습니다. 그래서 호스트 ps의 숫자를 컨테이너 ss 출력에 바로 대조하지 않습니다. 이번 검사는 같은 컨테이너 안에서 process.pid와 ps·ss를 비교합니다. 숫자 하나보다 어디에서 관찰했는지가 먼저입니다.

실행 계정은 파일을 읽거나 쓰는 주체를 연결하는 근거입니다. ps에서는 euid와 egid를 골라 현재 유효 사용자·그룹 번호를 읽습니다. app 파일의 소유자가 app이라는 사실이 프로세스도 app으로 실행됐음을 증명하지는 않습니다. Dockerfile의 USER 선언과 실행 중 UID를 각각 확인하면 설정 의도와 관찰 결과를 나누어 설명할 수 있습니다.

ps -p PID -o pid=,ppid=,euid=,egid=,stat=,args=는 지정 PID의 필드를 헤더 없이 표시합니다. PID 자리에는 관찰한 숫자를 넣습니다. stat의 S는 대기 상태 등을 나타내며 앱이 보안상 안전하다는 점수는 아닙니다. args는 실행 인자를 보여 주므로 실제 비밀을 인자로 넘기지 않는 습관이 필요합니다. 이번 앱은 더미 자료만 다루며 보고서에 불필요한 다른 프로세스 목록을 넣지 않습니다.

리스너와 연결의 필드 읽기

ss -ltnpH에서 l은 리스닝, t는 TCP, n은 숫자 주소·포트, p는 프로세스 정보, H는 헤더 생략입니다. LISTEN 행의 로컬 주소는 해당 소켓이 어떤 주소에서 요청을 받도록 열렸는지 보여 줍니다. 상대 주소가 와일드카드라고 외부와 연결됐다고 읽지 않습니다. 리스너 관찰과 이미 성립된 클라이언트 연결 관찰은 다른 질문입니다.

127.0.0.1:8765는 해당 네트워크 네임스페이스의 IPv4 루프백에만 바인딩한 주소입니다. 0.0.0.0:8765는 그 네임스페이스의 모든 IPv4 인터페이스를 대상으로 합니다. 이 차이를 HTTP 인증 유무와 혼동하지 않습니다. 현재 앱에는 인증이 없으므로 주소 범위를 좁히는 조치가 필요하지만, 루프백 바인딩 자체가 사용자별 접근 통제를 구현하는 것은 아닙니다.

컨테이너 안의 루프백은 호스트 루프백과 같지 않습니다. 이 실습은 포트를 게시하지 않고 network none을 씁니다. docker inspect로 게시·마운트 없음도 확인합니다. 컨테이너 ss만 보고 호스트의 모든 포트가 닫혔다고 결론 내리지 않습니다. 앱 리스너의 주소, 컨테이너 실행 설정, 호스트 경계는 보고서에서 각각 다른 근거로 연결합니다.

증거를 같은 실행에 묶기

observe.cjs의 process.pid는 현재 관찰 프로그램이자 리스너를 가진 Node 프로세스입니다. ss에서 8765 행을 골라 주소가 127.0.0.1이고 users 정보의 pid가 같은지 확인합니다. UID 10001도 함께 검사합니다. 단순히 node라는 이름이 있다는 이유로 다른 프로세스의 소켓을 앱의 소켓이라고 기록하지 않습니다. 같은 이름 프로세스가 여러 개일 때 특히 필요한 기준입니다.

HTTP 검사에서는 합성 자료를 POST로 생성해 201을 받고 같은 id를 GET으로 조회해 200과 같은 자료를 받습니다. 존재하지 않는 id는 404인지 봅니다. 이 근거는 앞 모듈의 function-call 증거와 달리 실제 루프백 HTTP 요청입니다. 그렇더라도 TLS, DNS, 외부 네트워크 또는 로그인 성공을 시험한 것은 아닙니다. 통신 검사의 범위를 작게 정확하게 적습니다.

E02에는 observedAt, environment, pid, uid, gid, ps, ss, address, http를 저장합니다. ps와 ss 원문을 남기면 필드 선택이나 파싱이 잘못됐는지 리뷰할 수 있습니다. PID와 상태는 실행마다 달라져 고정 숫자를 기대 출력으로 쓰지 않습니다. 따라하기의 합성 행은 파싱 연습이라고 명시하며 실제 Linux 관찰 원문을 대신하지 않습니다.

잘못된 바인딩을 찾아 고치기

starter의 observe.cjs는 0.0.0.0에 바인딩합니다. network none이라 외부 공개를 재현하지는 않지만 의도한 루프백 정책에 맞지 않습니다. check.sh는 loopback listener 검사에서 실패해야 합니다. server.listen에 넘기는 주소를 127.0.0.1로 고치고 같은 검사와 기존 앱 테스트를 유지합니다. 포트 숫자만 바꾸면 범위 문서와 검사 대상이 어긋납니다.

ss에서 프로세스 정보가 비어 있으면 권한 제한이나 관찰 대상의 차이를 확인합니다. 이 실습에서는 소켓 소유자인 app 자신이 관찰하므로 pid 필드가 보여야 한다는 계약을 둡니다. 필드가 없다고 앱이 없다는 결론을 내리거나 검사를 삭제하지 않습니다. 같은 컨테이너에서 실행됐는지, 정확한 명령인지, 앱이 리스닝 중인지 순서대로 확인합니다.

실패 메시지와 종료 범위

EADDRINUSE는 해당 주소·포트가 이미 사용 중일 수 있다는 뜻입니다. 이번 코드는 하나의 리스너만 만들고 마친 뒤 server.close를 호출합니다. 다른 프로그램을 종료해 빈 포트를 만들려 하지 않고 자신의 중복 실행 여부와 관찰 환경을 확인합니다. request timeout은 HTTP 요청이 정해진 시간 안에 끝나지 않았다는 단서입니다. 주소 오류와 응답 처리 오류를 나누어 살핍니다.

check.sh는 이미지 빌드, 컨테이너 생성, 경계 설정 확인, 실행, 종료 코드 검사, 증거 복사 순서입니다. docker start -a의 출력만 봐서는 내부 명령 전체 성공을 보장하지 못하므로 컨테이너 ExitCode도 확인합니다. 실패한 실행에서 마지막 PASS가 나오지 않으면 E02 성공으로 표시하지 않습니다. 진단 원문과 실패 검사 이름을 보관하고 수정 후 다시 실행합니다.

리스너는 finally에서 정상적으로 닫습니다. 요청에는 시간 제한이 있어 연결 대기가 계속 남지 않게 합니다. 컨테이너는 끝난 뒤 제거하며 강제 종료 명령을 사용하지 않습니다. 코드를 직접 바꾸어 계속 실행하는 서버로 만들면 이 레슨의 자동 종료 계약도 달라집니다. 관찰 도구가 새로운 장기 실행 서비스를 만들지 않도록 실행 수명을 함께 검토합니다.

리뷰에서 설명할 결론

완료 후에는 “어느 환경의 어떤 UID 프로세스가 어느 주소의 소켓을 열었고 어떤 요청이 성공했는가”를 한 문장으로 설명합니다. 비루트와 루프백 성공은 각각 계정·주소 근거로 뒷받침합니다. 인증·인가, 자료 영속성, 호스트 방화벽은 미검증이라고 남깁니다. ps의 다양한 상태와 옵션은 서재에서 확장하고 여기서는 근거 연결을 끝냅니다.

명령 옵션은 ps 문서와 ss 문서에 대조했습니다. 전체 목록을 외우기보다 목표 필드를 골라 관찰하고, 원문과 해석을 함께 보존하는 것이 이번 레슨의 실무 기준입니다.

따라하기

합성 ps 필드 읽기

아래 문자열은 파싱 연습용 합성 행입니다. 실제 프로세스 관찰 출력이 아닙니다. PID와 EUID를 별도 값으로 읽습니다.

line = '42 1 10001 10001 S node observe.cjs'
pid, ppid, euid, egid, state, command = line.split(maxsplit=5)
print('pid=' + pid, 'euid=' + euid)
print('command=' + command)

실행 결과

pid=42 euid=10001
command=node observe.cjs

합성 주소 비교하기

정확한 바인딩 문자열만 비교하는 연습입니다. 호스트 포트 공개 여부를 계산하지 않습니다.

for address in ['127.0.0.1:8765', '0.0.0.0:8765']:
    print(address, 'loopback=' + str(address == '127.0.0.1:8765'))

실행 결과

127.0.0.1:8765 loopback=True
0.0.0.0:8765 loopback=False

관찰 시점 안에서 검사하기

starter의 주소 결함으로 loopback listener가 실패하는지 확인합니다. observe.cjs의 server.listen 주소를 127.0.0.1로 고칩니다. 프로세스를 따로 종료할 필요 없이 검사 코드가 리스너를 닫습니다.

bash check.sh

E02 연결 검토하기

재실행 뒤 E02의 pid와 ps·ss 원문, uid, address, HTTP 생성·조회 결과를 대조합니다. PID 숫자와 시각은 실행마다 달라집니다. 마지막 PASS: process와 종료 코드 0을 완료 기준으로 삼습니다.

python3 -m json.tool linux-evidence.json

확인 문제

실습

observe.cjs의 전체 인터페이스 바인딩을 루프백으로 고칩니다. 기존 앱·범위 테스트를 유지합니다. bash check.sh는 실제 ps와 ss의 PID 연결, UID 10001, 127.0.0.1:8765, HTTP 생성·조회·404와 컨테이너 경계를 확인하고 E02를 복사합니다. E02가 보장하지 않는 항목도 두 가지 적습니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

기존 10개 테스트 및 E02 비루트·리스너·HTTP 검사 성공, 마지막 PASS: process

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • PID·실행 계정·리스닝 주소를 같은 실행의 근거로 연결하는 절차를 설명합니다.