Ubuntu APT 패키지 업데이트 내역 추적하기 — 로그, changelog, Launchpad, Snapshot

장애 원인을 찾다 보면 “이 라이브러리가 언제, 왜 바뀌었나”를 물어야 할 때가 있다. apt upgrade 화면만으로는 답을 알 수 없다. APT 사용 가이드의 기본 명령도 현재 상태만 보여준다. 답은 로컬 로그, changelog, Launchpad, Snapshot 서비스에 나뉘어 있다.

이 글에서는 패키지 변경 내역을 과거·현재·대기·원인 순으로 추적한다. 출력은 Ubuntu 24.04 테스트 서버와 26.04.1 rootfs에서 2026-11-04에 직접 실행한 결과다.

내 서버에서 언제 바뀌었나

APT 로그는 트랜잭션 단위, dpkg 로그는 패키지 단위로 남는다.

$ zcat /var/log/apt/history.log.1.gz | tail -4
Start-Date: 2026-10-30  17:30:43
Commandline: /usr/bin/unattended-upgrade
Upgrade: libbrotli1:amd64 (1.1.0-2build2, 1.1.0-2ubuntu0.1)
End-Date: 2026-10-30  17:30:49

$ zgrep -h ' upgrade openssl:' /var/log/dpkg.log* | sort
2026-08-10 16:59:39 upgrade openssl:amd64 3.0.13-0ubuntu3 3.0.13-0ubuntu3.12
2026-08-27 06:20:15 upgrade openssl:amd64 3.0.13-0ubuntu3.12 3.0.13-0ubuntu3.15
2026-10-04 06:38:00 upgrade openssl:amd64 3.0.13-0ubuntu3.15 3.0.13-0ubuntu3.16
로그내용용도
/var/log/apt/history.log*트랜잭션별 명령·요청자·패키지누가 어떤 명령으로 바꿨는지 확인
/var/log/apt/term.log*설치 중 터미널 출력 (adm 그룹만 읽기)postinst 오류 확인
/var/log/dpkg.log*패키지별 상태 변화특정 패키지의 변경 시각 확인
/var/log/unattended-upgrades/자동 업데이트 실행 기록자동 적용 여부 확인
$ zcat -f /var/log/apt/history.log* | grep -E '^(Commandline|Requested-By)' | cut -d: -f1 | sort | uniq -c
    264 Commandline
     41 Requested-By

Requested-By는 sudo로 실행한 트랜잭션에만 남는다. 이 서버에서는 264건 중 41건이었고, 없는 항목에는 위의 unattended-upgrade 같은 자동 실행이 포함된다.

지금 어떤 버전이 어디서 왔나

$ apt-cache policy openssl
openssl:
  Installed: 3.0.13-0ubuntu3.16
  Candidate: 3.0.13-0ubuntu3.16
  Version table:
 *** 3.0.13-0ubuntu3.16 500
        500 http://kr.archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu noble-security/main amd64 Packages
        100 /var/lib/dpkg/status
     3.0.13-0ubuntu3 500
        500 http://kr.archive.ubuntu.com/ubuntu noble/main amd64 Packages

$ apt list -a openssl 2>/dev/null
Listing...
openssl/noble-updates,noble-security,now 3.0.13-0ubuntu3.16 amd64 [installed,automatic]
openssl/noble 3.0.13-0ubuntu3 amd64

noble은 릴리스 시점의 버전이고, -security와 -updates는 이후의 갱신이다. 일반 미러는 중간 버전을 지우므로 위 목록에는 .12와 .15가 없다.

왜 바뀌었나: changelog

$ apt-get changelog openssl 2>/dev/null | head -8
Get:1 https://changelogs.ubuntu.com openssl 3.0.13-0ubuntu3.16 Changelog [156 kB]
openssl (3.0.13-0ubuntu3.16) noble-security; urgency=medium

  * SECURITY UPDATE: Excessive Memory Allocation in Relative CRLDP Processing
    - debian/patches/CVE-2026-35189.patch: Defer computation of relative CRLDP
      names in crypto/x509/v3_crld.c, crypto/x509/v3_purp.c,
      crypto/x509/x509_vfy.c, include/crypto/x509.h.
    - CVE-2026-35189

apt-get changelog는 HTTPS로 내려받는다. 최소 이미지에서는 ca-certificates가 없어 아래처럼 내용 없이 끝난다.

# ca-certificates가 없는 ubuntu-base rootfs
$ apt-get changelog curl
Ign:1 https://changelogs.ubuntu.com curl 8.18.0-1ubuntu2.8 Changelog

업그레이드 전에 무엇이 바뀔지 보기

대기 중인 패키지의 pocket과 새 CVE를 한 번에 보는 스크립트다. 새로 추가된 changelog 항목만 읽는다.

#!/usr/bin/env bash
# 업그레이드 대기 패키지의 pocket, 버전 변화, 새 changelog 항목의 CVE를 출력한다
set -uo pipefail
apt list --upgradable 2>/dev/null | tail -n +2 | while IFS= read -r line; do
  pkg=${line%%/*}
  pocket=$(cut -d/ -f2 <<<"$line" | cut -d' ' -f1)
  new=$(awk '{print $2}' <<<"$line")
  old=$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null)
  cves=$(apt-get changelog "$pkg" 2>/dev/null | {
    while IFS= read -r l; do
      if [[ $l =~ ^[a-z0-9.+-]+\ \(([^\)]+)\) ]]; then
        # 바이너리와 소스의 epoch가 다를 수 있어 epoch를 빼고 비교한다
        dpkg --compare-versions "${BASH_REMATCH[1]#*:}" le "${old#*:}" && break
      fi
      printf '%s\n' "$l"
    done; } | grep -oE 'CVE-[0-9]+-[0-9]+' | sort -u | paste -sd, -)
  printf '%-14s %-34s %s -> %s  %s\n' "$pkg" "$pocket" "$old" "$new" "${cves:--}"
done

26.04.1 rootfs에서 실행하면 업그레이드 대상 29개 중 20개에서 CVE 번호가 나온다. 출력은 긴 줄을 뺀 일부다.

$ sudo apt-get update -qq && apt-changes
diffutils      resolute-updates,resolute-security 1:3.12-1 -> 1:3.12-1ubuntu0.1  CVE-2026-53910
gnu-coreutils  resolute-updates,resolute-security 9.7-3ubuntu2 -> 9.7-3ubuntu2.1  CVE-2025-5278,CVE-2026-56391
gpgv           resolute-updates,resolute-security 2.4.8-4ubuntu3 -> 2.4.8-4ubuntu3.1  CVE-2026-57062
libattr1       resolute-updates,resolute-security 1:2.5.2-4 -> 1:2.5.2-4ubuntu0.1  CVE-2026-54371
libbz2-1.0     resolute-updates,resolute-security 1.0.8-6build2 -> 1.0.8-6ubuntu0.1  CVE-2026-42250
libgcrypt20    resolute-updates,resolute-security 1.12.0-2ubuntu1 -> 1.12.0-2ubuntu1.1  CVE-2024-2236
libaudit1      resolute-updates                   1:4.1.2-1build1 -> 1:4.1.2-1ubuntu0.1  -
liblzma5       resolute-updates,resolute-security 5.8.3-1 -> 5.8.3-1ubuntu0.1  -
rust-coreutils resolute-updates                   0.8.0-0ubuntu3 -> 0.10.0-1ubuntu2~26.04.1  -

-는 “보안 수정이 아니다”가 아니라 “CVE 번호를 찾지 못했다”는 뜻이다. liblzma5처럼 -security에 있으면 보안 업데이트로 취급한다.

언제 배포됐나: Launchpad

일반 미러에서 사라진 버전까지 포함한 배포 이력은 Launchpad API로 조회한다.

#!/usr/bin/env python3
# 패키지의 시리즈별 배포 이력을 출력한다: lp-history openssl noble
import json, sys, urllib.parse, urllib.request

pkg, series = sys.argv[1], sys.argv[2]
query = urllib.parse.urlencode({
    "ws.op": "getPublishedSources",
    "source_name": pkg,
    "exact_match": "true",
    "distro_series": f"https://api.launchpad.net/devel/ubuntu/{series}",
    "order_by_date": "true",
    "ws.size": 8,
})
url = f"https://api.launchpad.net/devel/ubuntu/+archive/primary?{query}"
for e in json.load(urllib.request.urlopen(url))["entries"]:
    print(e["date_published"][:10], e["source_package_version"], e["pocket"], e["status"])
$ lp-history openssl noble
2026-09-29 3.0.13-0ubuntu3.16 Updates Published
2026-09-29 3.0.13-0ubuntu3.16 Security Published
2026-08-25 3.0.13-0ubuntu3.15 Updates Superseded
2026-08-25 3.0.13-0ubuntu3.15 Security Superseded
2026-08-12 3.0.13-0ubuntu3.13 Proposed Deleted
2026-08-06 3.0.13-0ubuntu3.10 Proposed Superseded
2026-07-30 3.0.13-0ubuntu3.12 Updates Superseded
2026-07-30 3.0.13-0ubuntu3.12 Security Superseded

.16은 9월 29일에 배포됐고, 앞의 dpkg 로그를 보면 이 서버에는 10월 4일에 적용됐다. 두 날짜의 차이가 패치 적용 지연이다.

특정 CVE가 해결됐나

$ pro fix CVE-2026-35189 --dry-run
CVE-2026-35189: OpenSSL vulnerabilities
 - https://ubuntu.com/security/CVE-2026-35189
2 affected source packages are installed: edk2, openssl
(1/2) edk2:
Ubuntu security engineers are investigating this issue.
(2/2) openssl:
A fix is available in Ubuntu standard updates.
The update is already installed.
1 package is still affected: edk2
✘ CVE-2026-35189 is not resolved.

openssl은 수정됐지만 edk2가 영향 대상으로 남아 있다. openssl의 changelog만 보면 이 차이를 알 수 없다.

과거 시점의 저장소 보기: Snapshot

Snapshot 서비스는 특정 시각의 아카이브를 그대로 보여준다. 24.04 이상의 APT는 별도 저장소 설정 없이 지원한다.

$ sudo apt update --snapshot 20260601T000000Z
$ apt-cache policy curl --snapshot 20260601T000000Z | sed -n 1,6p
curl:
  Installed: (none)
  Candidate: 8.18.0-1ubuntu2.1
  Version table:
     8.18.0-1ubuntu2.1 500
        500 https://snapshot.ubuntu.com/ubuntu/20260601T000000Z resolute-updates/main amd64 Packages

# --snapshot 없이 조회하면 현재 후보가 나온다
$ apt-cache policy curl | sed -n 3p
  Candidate: 8.18.0-1ubuntu2.8

시점별로 같은 패키지의 후보를 비교하면 언제의 저장소에서 문제가 시작됐는지 좁힐 수 있다. 아래는 실제로 설치하지 않는 시뮬레이션이다.

$ apt-get -s install curl --update --snapshot 20260601T000000Z | grep '^Inst curl'
Inst curl (8.18.0-1ubuntu2.1 Ubuntu:26.04/resolute-updates, Ubuntu:26.04/resolute-security [amd64])
$ apt-get -s install curl --update --snapshot 20261001T000000Z | grep '^Inst curl'
Inst curl (8.18.0-1ubuntu2.7 Ubuntu:26.04/resolute-updates, Ubuntu:26.04/resolute-security [amd64])
$ apt-get update -qq && apt-get -s install curl | grep '^Inst curl'
Inst curl (8.18.0-1ubuntu2.8 Ubuntu:26.04/resolute-updates, Ubuntu:26.04/resolute-security [amd64])

시스템 전체를 한 시점에 고정하려면 설정 파일을 둔다. 스테이징과 운영 서버의 패키지 버전을 맞출 때 쓴다.

APT::Snapshot "20260601T000000Z";
$ sudo apt-get update -qq
$ apt-cache policy curl | sed -n 3p
  Candidate: 8.18.0-1ubuntu2.1
$ apt-get -s upgrade | tail -1
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

주의사항

지시이유
50snapshot을 둔 채로 방치하지 말 것고정 시점 이후의 보안 패치를 받지 못한다. 위 출력에서 upgrade는 0건이었고, unattended-upgrades도 같은 설정을 따른다
오래된 이력이 필요하면 로그를 따로 보관할 것apt와 dpkg 로그의 logrotate 설정은 monthly, rotate 12다
changelog 첫 항목만 보고 판단하지 말 것한 번의 업그레이드가 여러 항목을 건너뛸 수 있다. apt-changes는 설치 버전까지의 항목을 모두 읽는다
버전 비교에 문자열 일치를 쓰지 말 것no-change rebuild 버전(2.7.14build2)은 changelog에 없을 수 있다. 일치 비교는 전체 이력의 CVE를 잘못 모은다. dpkg --compare-versions를 쓴다
바이너리 버전과 changelog 버전의 epoch를 그대로 비교하지 말 것bsdutils는 1:2.41.3이지만 소스 버전에는 epoch가 없어 비교가 첫 줄에서 끝났다
apt-cache policy의 phased 표시를 확인할 것phased update는 일부 시스템에만 먼저 배포된다. 서버마다 후보 버전이 다르면 먼저 의심한다. 이번 측정에서는 대기 156건 중 해당 항목이 없어 출력을 확인하지 못했다

마무리

질문도구
내 서버에서 언제 바뀌었나zgrep ' upgrade 패키지:' /var/log/dpkg.log*
누가 어떤 명령으로 바꿨나/var/log/apt/history.log*
지금 버전은 어디서 왔나apt-cache policy, apt list -a
왜 바뀌었나apt-get changelog
언제 배포됐나Launchpad getPublishedSources
CVE가 해결됐나pro fix CVE-... --dry-run
그 시점 저장소는 어땠나apt update --snapshot

패키지를 교체한 뒤 실행 중인 프로세스까지 확인하려면 needrestart를 함께 쓴다.

참고

답글 남기기