베이지 배경 위에 도장, 기한을 가리키는 원형 계기, 걸린 문서 한 장, 초록 상자를 잇는 관이 놓인 일러스트로 서명 키의 유효기간과 저장소 설정 전달 경로를 상징한다

apt update에서 ROS 2 저장소 서명 오류(F42ED6FBAB17C654)가 날 때 공식 문서의 ros2-apt-source 단계는 무엇을 바꿀까: 기계 수와 저장소 설정 변경 횟수로 키 관리 수작업 계산하기


Read in English

로봇 PC에서 sudo apt update를 돌렸더니 ROS 2 저장소 줄에서 The following signatures were invalid: EXPKEYSIG F42ED6FBAB17C654 Open Robotics가 찍히고 패키지 설치가 막힌다. apt update에서 ROS 2 저장소 서명 오류(F42ED6FBAB17C654)가 나는 이 상황은 2021년 5월 Robotics Stack Exchange에 올라온 질문(Stack Exchange API 원문) 하나만 해도 조회가 8천 회를 넘었다. 당시 채택 답은 키 파일을 다시 받아 덮어쓰라는 한 줄이었다. 현재 공식 설치 문서는 그 자리에 ros2-apt-source라는 패키지 설치 단계를 둔다. 이 단계가 정확히 무엇을 바꾸는지, 여러 대를 관리할 때 정말 손이 덜 가는지가 이 글의 질문이다.

답을 먼저 좁혀 둔다. F42ED6FBAB17C654는 2019년에 만들어진 뒤 한 번도 교체되지 않은 같은 키이고, 오류는 기계에 있는 그 키의 사본이 만료돼서 난다. ros2-apt-source도 키를 바꾸지 않는다. 키와 저장소 설정이 기계에 도착하는 경로를 사람의 curl에서 apt가 관리하는 패키지로 옮길 뿐이다. 그래서 이득은 조건부다. 앞으로 설정 변경이 두 번 이상 있고, 기계가 만료 전에 업데이트를 받는다는 조건이 붙는다. 이 패키지는 예전 ros2.list를 지우지 않고, 그 파일이 남아 있으면 apt가 멈춘다. 이전 색인이 남아 있는 기계가 갱신을 놓치면 apt-get update가 종료 코드 0을 내면서 경고만 남긴다는 점도 오프라인 컨테이너에서 확인했다. 아래 내용은 2026-10-18에 확인한 공식 문서·패키지·키 파일, 직접 만든 산술 예제, 그리고 apt 설정 동작을 컨테이너에서 5회 재현한 결과다. 실제 packages.ros.org 서버나 로봇 하드웨어에서 실행한 결과는 아니다.

EXPKEYSIG F42ED6FBAB17C654를 한 단어씩 읽으면

터미널에서 sudo로 명령을 실행해 봤고 apt가 “저장소 목록을 읽고 → 색인을 내려받고 → 패키지를 설치한다”는 정도만 알면 따라올 수 있다. 암호학 수식은 필요 없다.

apt가 패키지를 받아 오는 서버 주소와 그 설정을 저장소(repository) 또는 소스(source)라고 부른다. apt는 저장소에서 색인을 받기 전에 InRelease부터 확인한다. 색인 파일 목록과 해시가 담겨 있고, 서명이 붙은 파일이다. 저장소 운영자(여기서는 Open Robotics)는 비밀키로 서명하고, 사용자 기계는 짝이 되는 공개키(public key) 사본으로 그 서명을 확인한다. 키는 40자리 16진수인 지문(fingerprint)으로 식별하며, 오류에 나오는 F42ED6FBAB17C654는 지문 C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654의 끝 16자리, 즉 긴 키 ID(long key ID)다.

EXPKEYSIG는 “서명은 이 키로 한 것이 맞는데, 내 기계에 있는 이 키의 사본이 만료됐다”는 뜻이다. 비슷해 보이는 NO_PUBKEY F42ED6FBAB17C654는 공개키 자체가 기계에 없다는 다른 증상이다. 2021년 질문의 실제 출력에는 W: GPG error ... EXPKEYSIG 줄과 함께 E: The repository ... is not signed. 줄도 있었다.

도장에 빗대면 이렇다. 서명 키는 도장이고, 기계의 공개키 사본은 그 도장을 대조할 인감증명서다. 만료는 증명서 유효기간이 지난 상태다. 다만 비유와 실제가 갈리는 지점이 있다. 만료된 것은 도장(비밀키) 자체가 아니라 운영자가 정해 둔 키의 유효기간이다. 운영자가 유효기간을 늘린 공개키를 다시 배포하면 같은 도장이 다시 유효해진다. 새 도장을 파는 “키 교체”와는 다르다.

같은 키가 2019년부터 만료일만 세 번 가졌다

ROS 서명 키의 공개 사본은 rosdistro 저장소의 ros.key에 있다. 이 글을 준비하며 세 시점의 커밋에서 ros.key를 내려받아 gpg --show-keys로 읽었다.

ros.key 커밋 지문 생성일 만료일(UTC)
89b6383 (2019) C1CF…C654 2019-05-30 2021-05-29 00:40:54
8baf090 (2021) 같음 같음 2025-06-01 05:59:45
eb71c28 (2025) 같음 같음 2030-06-01 12:39:34

지문과 생성일이 셋 다 같다. 바뀐 것은 만료일뿐이다. 2021년 사건 공지(ROS GPG Key Expiration Incident)에 따르면 2019년 보안 사고 뒤 2년 만료 키를 배포했고, 만료 전에 연장하는 일을 놓쳤다. 공지는 만료일을 늘리려면 새 공개키를 발급해야 하고 사용자가 그것을 다시 받아야 한다고 설명했다. 2025년에는 6월 1일 만료를 앞두고 이전 가이드가 5월 26일 나왔고, 다음 날 ros.key가 2030년 만료본으로 바뀌었다.

공지 문구와 실제 키가 어긋나는 곳도 하나 있다. 2021년 공지는 연장된 키가 5년 뒤 만료된다고 했지만, 2021년 커밋 키의 실제 만료일은 약 4년 뒤인 2025-06-01이었다. 다음 만료 시점을 계획할 때는 공지보다 자기 기계의 키 파일에서 읽은 날짜를 믿는 편이 안전하다. 지금 알려진 다음 만료는 2030-06-01 12:39:34 UTC다.

손으로 만든 두 파일과 패키지가 까는 세 파일

2025년 5월 이전 공식 문서(Humble 문서 d96b213 L10–L21)는 사람이 두 파일을 만들게 했다. curl로 받은 키를 /usr/share/keyrings/ros-archive-keyring.gpg에 저장하고, signed-by=가 들어간 한 줄을 /etc/apt/sources.list.d/ros2.list에 쓰는 방식이다. 공개키를 담은 파일을 키링(keyring)이라 부르고, Signed-By(또는 한 줄 표기의 signed-by=)는 “이 저장소는 이 키로 서명된 것만 믿는다”고 저장소마다 지정하는 옵션이다(sources.list(5)). 이 구조에서 키 사본을 새로 고치는 일은 전부 사람 몫이었다.

현재 문서(Rolling 문서 dfb668f L83–L95, Jazzy 문서 c0ff666 L10–L20)는 GitHub 릴리스에서 ros2-apt-source_<버전>.<코드네임>_all.deb을 curl -L로 받아 sudo dpkg -i로 설치하게 한다. .deb은 파일 묶음과 메타데이터를 담은 패키지 파일이고, dpkg -i는 내려받은 .deb을 직접 설치하는 명령이다. 문서는 이 패키지가 ROS 저장소를 설정하며, 패키지 새 버전이 ROS 저장소에 배포되면 설정 갱신이 자동으로 일어난다고 적는다.

ros-apt-source 저장소의 1.0.0~1.3.0 jammy·noble용 .deb 8개를 열어 보면, 문서 파일을 빼고 저장소 설정에 쓰이는 경로는 모두 같은 세 곳이다.

항목 예전 수동 방식 ros2-apt-source
키링 /usr/share/keyrings/ros-archive-keyring.gpg /usr/share/keyrings/ros2-archive-keyring.gpg (ros → ros2로 이름이 다름)
저장소 파일 /etc/apt/sources.list.d/ros2.list 한 줄, signed-by= 키 파일 경로 /usr/share/ros-apt-source/ros2.sources (deb822 형식, Signed-By:에 키 블록 자체를 내장)
sources.list.d에 보이는 것 ros2.list 실제 파일 ros2.sources → 위 파일을 가리키는 심볼릭 링크
갱신 사람이 키 파일을 다시 받아 덮어씀 저장소에 올라온 패키지 새 버전을 apt로 받음
되돌리기 파일 삭제 sudo apt remove ros2-apt-source 뒤 apt update·autoremove·upgrade (문서 L234–L241)

deb822 형식은 Types:, URIs:, Suites:, Components: 같은 필드를 여러 줄로 쓰는 저장소 표기이고, 심볼릭 링크(symbolic link)는 다른 파일을 가리키는 바로가기 파일이다. 저장소 규칙 파일(rules L26–L37)을 보면 키 블록 내장은 22.04 Jammy 이상에만 적용되고, 20.04 Focal용은 키링 경로를 쓴다(Focal .deb은 열어 보지 않았다). 키가 소스 파일에 들어 있는데도 키링 파일을 따로 두는 이유는 README L24–L27에 있다. 소스 파일에서 키를 꺼내지 않고도 검증할 수 있게 하려는 것이다.

예전 수동 방식(ros-archive-keyring.gpg, ros2.list)과 ros2-apt-source가 설치하는 세 파일(ros2-archive-keyring.gpg, ros2.sources, 심볼릭 링크)을 나란히 비교하고, 두 방식 공존 시 apt가 저장소 설정을 읽는 단계에서 Conflicting values set for option Signed-By 오류를 낸다는 경고(실험의 apt-cache policy는 종료 코드 100)를 담은 개념도

여기서 두 가지가 계산에 직접 영향을 준다.

첫째, 이 패키지는 예전 파일을 지우지 않는다. .deb의 control 아카이브에는 control과 md5sums만 있고 설치 때 실행되는 스크립트(preinst, postinst)가 없다. 이전 가이드도 예전 저장소 설정을 지우지 않으면 키 충돌로 apt가 깨진다고 경고한다. apt 2.8.2 소스(debmetaindex.cc L809–L824)를 보면, 같은 저장소에 정규화한 값이 서로 다른 두 번째 Signed-By가 들어오면 Conflicting values set for option Signed-By 오류를 낸다. 키 파일 경로와 내장 키 블록은 문자열이 다르므로, 둘이 같은 키를 담고 있어도 충돌로 판정될 수 있다(L60–L94).

둘째, 패키지 버전이 올라가는 동안 키는 아직 한 번도 바뀌지 않았다. 릴리스는 1.0.0(2025-04-30), 1.1.0(2025-05-24), 1.2.0(2026-04-23), 1.3.0(2026-09-10)로 이어졌는데, 8개 .deb의 키링 바이트가 모두 같고 현재 ros.key와 SHA-256도 같다. 처음부터 2030년 만료본을 담고 출발했기 때문이다. “패키지 새 버전으로 키가 자동 갱신되는” 경로는 설계와 문서의 약속이며, 실제로 쓰이는 첫 사례는 2030년 연장 때가 될 것이다. 그 약속에도 조건이 있다. 기계가 예전 사본이 만료되기 전에 apt update와 apt upgrade로 새 버전을 받아야 한다. 이전 가이드 역시 “사용자가 시스템을 최신 상태로 유지하는 한”이라는 조건을 붙였다. 2026-10-18 기준으로 ROS apt 저장소에도 1.3.0 패키지가 올라와 있었고, 그 해시는 GitHub 릴리스의 같은 파일과 일치했다.

기계 6대라면 손이 몇 번 가는가

이제 질문을 숫자로 바꿔 보자. 단위는 “기계 한 대에서 사람이 하는 저장소 설정 작업 1회”로 둔다. 두 방식 모두 이미 예전 수동 방식으로 설정된 기계에서 출발한다고 가정한다.

  • n: 관리하는 기계 수
  • k: 앞으로 관리 기간 동안 일어날 만료일 연장(또는 키·저장소 설정 변경) 횟수
  • m: 변경 시점에 꺼져 있거나 업데이트를 받지 못해 사람이 따로 고쳐야 하는 기계 수

수동 방식 A는 변경 때마다 기계마다 1회씩 손이 가므로 A = n × k다. 패키지 방식 B는 이전 작업(설치·예전 파일 삭제·확인을 묶어 1회)을 기계마다 한 번 하고, 이후 변경은 apt가 가져온다. 다만 갱신을 놓친 기계는 사람이 고쳐야 하므로 B = n + m이다. m = 0이면 A − B = n × (k − 1)이 된다.

이 산술은 example.py로 7개 경우를 검산했고, 기대값과 실제값이 모두 일치한 기록은 verification.json에 있다(Python 3.12.3, python3 -I example.py, 종료 코드 0). apt나 로봇을 실행한 것은 아니며, 사칙연산 검산이다. example.py는 m = 0인 경우만 다룬다. m 항은 이 글에서 덧붙인 확장이다.

경우 A = n × k B = n + m 판단
6대, k = 2, m = 0 12 6 B가 6회 적다
6대, k = 1, m = 0 6 6 횟수는 같다
1대, k = 1, m = 0 1 1 같다
6대, k = 0, m = 0 0 6 B가 6회 많다

k가 2 이상이어야 횟수로 이득이 생긴다. 키 이력을 보면 2019년부터 2026년까지 만료 사건은 두 번(2021, 2025) 있었고, 다음 예정은 2030년 한 번이다. k는 미래의 사건 수이므로 정답이 없다. 지금까지 키가 유효했던 기간은 처음 2년(2019→2021), 다음 4년(2021→2025)이었고, 현재 사본은 2025년 연장 뒤 5년인 2030년까지다. 기계를 몇 년 더 쓸지와 이 간격을 놓고 독자가 골라야 하는 가정값이다.

직접 넣어 볼 세 가지 경우

로봇 PC 4대와 개발 PC 2대를 2030년 연장 한 번만 겪고 교체할 계획이라면? n = 6, k = 1이므로 A = 6, B = 6이다. 횟수로는 차이가 없다. 남는 이득은 2030년에 어떤 명령을 쳐야 하는지 기억하지 않아도 된다는 질적인 차이뿐이고, 그마저 여섯 대가 만료 전에 업데이트를 받아야 성립한다.

같은 6대 중 1대가 연장 시점에 창고에서 꺼져 있었다면? m = 1이므로 B = 6 + 1 = 7이다. k = 1인 이 경우에는 A(6회)보다 한 번 많다. 꺼져 있는 시간이 긴 장비가 많을수록 B의 이득은 줄어든다.

아예 새로 설치하는 기계라면? “이미 수동 방식으로 설정됨”이라는 출발 가정이 깨진다. 새 기계는 어느 방식이든 초기 설정이 한 번 필요하므로, k = 0이어도 패키지 방식이 손해가 아니다. 표의 마지막 줄 같은 손해는 이미 수동 방식으로 잘 돌아가는 기계를 옮길 때만 생긴다.

오프라인 컨테이너에서 확인한 apt의 반응

위 계산은 apt가 문서대로 움직인다는 가정 위에 서 있다. 문서만으로는 확정하기 어려운 부분이 세 군데 있어서 컨테이너에서 직접 재현했다. 첫째는 공식 이미지에 실제로 깔린 파일과 그 점검 방법이고, 둘째는 예전 파일과 새 파일이 함께 있을 때 apt가 멈추는지다. 셋째는 갱신을 놓친 기계에서 apt-get update가 어떤 종료 코드를 내는지인데, 종료 코드(exit code)는 명령이 끝나며 돌려주는 숫자로 0이 성공을 뜻한다.

실험 환경은 다음과 같다. 공식 ros:jazzy-ros-base 이미지를 digest로 고정하고 네트워크 없이 1 CPU·1 GiB로 실행했으며, 같은 스크립트를 5회 반복했다. 이미지에 들어 있는 apt 2.8.3과 gpg 2.4.4, dpkg-deb만 사용했다. 결과 파일에는 RMW(ROS 노드 간 통신 구현체, ROS middleware)가 기본값 rmw_fastrtps_cpp로 기록돼 있지만, 이 실험은 ROS 노드를 띄우지 않으므로 결과와 관계가 없다. 시스템 설정을 건드리지 않도록 기계마다 apt 설정·색인·키 디렉터리를 /tmp 아래로 격리했다. 두 번째 질문의 파일 조합 다섯 가지는 이 격리 설정에서 apt-cache policy를 실행해 재었다. apt가 저장소 설정을 읽는 단계에서 내는 종료 코드와 첫 E:·W: 줄을 기록한 것이고, 네트워크 없이 돌렸으므로 apt update가 색인을 내려받는 단계까지 간 결과는 아니다. 아래 표에서 조합별 “종료 코드”는 이 apt-cache policy의 값이다. 세 번째 질문은 실험용 ed25519 키(수명 6초)와 file: 저장소로 재현했다. 기계는 7대를 두었다. 수동 방식 A 3대, 패키지 방식 B 3대, 그리고 첫 연장 때만 꺼져 있던 패키지 방식 B_off 1대다. 같은 키의 만료일을 두 번 연장하고(k = 2), 켜져 있는 B 기계는 사람 없이 apt로 새 키링 패키지를 받게 했다. 사람 손 횟수는 스크립트가 작업을 실행할 때마다 기계별 카운터를 올려 센 값이다.

확인한 것 plan.md 예측 실측 (5회 모두 같은 값)
이미지의 ros2-apt-source 버전 예측하지 않음 1.2.0~noble
/etc/apt/sources.list.d/ros2.sources 심볼릭 링크, 소유 패키지 ros2-apt-source 심볼릭 링크 → /usr/share/ros-apt-source/ros2.sources, 소유 패키지 ros2-apt-source
키링 지문·만료 C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654, 2030-06-01 12:39:34 UTC, 1,167바이트 같음 (RSA 4096, 생성 2019-05-30), Signed-By: 내장 키도 같은 지문
예전 ros2.list·ros-archive-keyring.gpg 없음 둘 다 없음
grep -rl / grep -Rl packages.ros.org /etc/apt/sources.list.d/ 0개 / 1개 0개(종료 코드 1) / 1개(ros2.sources)
새 파일만 정상 종료 코드 0
예전 파일만 정상 종료 코드 0
예전 + 새 파일 Conflicting values set for option Signed-By 종료 코드 100, 같은 오류
예전 줄이 패키지 키링 경로를 가리킴 + 새 파일 충돌 종료 코드 100, 같은 오류
같은 키의 예전 줄 두 개 configured multiple times 경고 종료 코드 0, 같은 경고
사람 손 횟수 (n = 3, k = 2) A 3×2 = 6, B 3, B_off 2 A 6, B 3, B_off 2
B_off의 첫 만료 후 apt-get update EXPKEYSIG, 종료 코드는 예측하지 않음 EXPKEYSIG, 종료 코드 0, 이전 색인 사용 경고
두 번째 연장 뒤 7대 예측 항목 없음 7대 모두 종료 코드 0, EXPKEYSIG 없음
이전 색인이 있는 기계, 낡은 키 사본 예측 항목 없음 종료 코드 0, EXPKEYSIG, 이전 색인 사용, 패키지 계속 보임
이전 색인이 없는 새 기계, 낡은 키 사본 is not signed 오류 함께 발생 종료 코드 100, EXPKEYSIG, is not signed, 색인 파일 0개, 패키지 안 보임

5회 실행은 모두 종료 코드 0으로 끝났고 실행 시간은 18.91~19.43초였다. 실행마다 달라진 것은 새로 만든 실험 키의 ID와 그 키의 만료 시각, 경과 시간 t 값뿐이다. 아래는 1회차 원본 출력에서 판단을 바꾸는 줄만 그대로 옮긴 것이다. 메시지는 실험 스크립트가 앞부분만 잘라 기록한다.

{"part": "image", "grep": "grep -rl packages.ros.org /etc/apt/sources.list.d/", "exit_code": 1, "files_found": 0, "files": [], "t": 0.09}
{"part": "image", "grep": "grep -Rl packages.ros.org /etc/apt/sources.list.d/", "exit_code": 0, "files_found": 1, "files": ["/etc/apt/sources.list.d/ros2.sources"], "t": 0.09}
{"part": "conflict", "case": "old_and_new", "files": ["ros2.list", "ros2.sources"], "exit_code": 100, "first_message": "E: Conflicting values set for option Signed-By regarding source http://packages.ros.org/ros2/ubuntu/ noble: /tmp/lab/conflict/ros-archive-keyring.gpg != -----BE", "conflicting_signed_by": true, "configured_multiple_times": false, "t": 0.13}
{"part": "lab", "stage": "event1", "same_fingerprint_as_created": true, "upgraded_by_apt": {"B1": true, "B2": true, "B3": true, "B_off": null}, "upgrades_finished_before_old_expiry": true, "after_expiry_update_exit": {"A1": 0, "A2": 0, "A3": 0, "B1": 0, "B2": 0, "B3": 0, "B_off": 0}, "expkeysig_machines": ["B_off"], "previous_index_used_machines": ["B_off"], "failed_messages": {"B_off": ["W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: file:/tmp/lab/lab/re", "W: Failed to fetch file:/tmp/lab/lab/repo/dists/lab/InRelease  The following signatures were invalid: EXPKEYSIG C125E13F35D4DF59 HuntLab Lab Archive <lab@exampl", "W: Some index files failed to download. They have been ignored, or old ones used instead."]}, "repaired_by_human": ["B_off"], "recheck_exit_after_repair": {"B_off": 0}, "t": 7.2}
{"part": "lab", "stage": "summary", "n_per_group": 3, "k_events": 2, "human_touches_by_machine": {"A1": 2, "A2": 2, "A3": 2, "B1": 1, "B2": 1, "B3": 1, "B_off": 2}, "human_touches_by_group": {"A_manual": 6, "B_package": 3, "B_package_offline_at_event1": 2}, "A_total_3_machines": 6, "B_total_3_online_machines": 3, "B_offline_machine_total": 2, "one_A_machine_total": 2, "t": 18.65}

예측한 항목은 모두 예측대로 나왔다. 사람 손 횟수가 공식과 같은 것은 놀랄 일이 아니다. 실험 설계가 공식과 같은 가정(이전 1회, 놓친 기계 복구 1회)으로 카운터를 올리기 때문이다. 실험이 계산에 보탠 것은 횟수가 아니라 apt의 실제 반응이다. 켜져 있던 B 세 대는 사람 없이 새 키 사본을 받아 만료를 넘겼고, 꺼져 있던 한 대는 EXPKEYSIG를 냈다. 그 대가로 B_off의 사람 손은 B 기계 한 대보다 한 번 많은 2회가 됐다. 공식의 m 항이 실제로 생긴 것이다.

판단을 바꾸는 줄은 B_off의 종료 코드 0이다. plan.md는 이 값을 예측하지 않고 측정 대상으로 남겨 두었다. 이전 색인이 있는 기계에서 apt-get update는 서명 검증에 실패해도 경고(W:)만 남기고 예전 색인을 계속 쓰며, 패키지도 그대로 보인다. 스크립트나 자동화가 종료 코드만 확인한다면 갱신을 놓친 기계는 정상처럼 보인다는 뜻이다. 실험에서 B_off를 잡아낸 것도 종료 코드가 아니라 출력의 EXPKEYSIG 문자열이었다. 반대로 색인이 한 번도 없는 새 기계는 종료 코드 100으로 바로 실패했다. 공식 설치 문서의 첫 명령이 sudo apt update && sudo apt install curl -y인데, &&는 앞 명령이 0이 아닌 코드로 끝나면 뒤 명령을 실행하지 않는다. 새 기계에 낡은 키 사본이 든 예전 설정을 먼저 깔아 두면 curl 설치 단계에서 멈출 수 있다.

예전 파일과 새 파일의 공존도 실측에서 그대로 확인됐다. 예전 줄이 패키지의 키링 경로를 가리키도록 고쳐도 종료 코드 100이었다. 경로 문자열과 내장 키 블록이 다르기 때문이다. 공식 이미지에는 예전 파일이 없었지만, 이미지의 ros2-apt-source는 조사 시점의 최신 1.3.0이 아니라 1.2.0~noble이었다. 이 차이는 시험 실행에서 처음 알았고, 그래서 버전은 예측하지 않고 측정값으로만 기록했다. 점검 명령의 함정도 하나 드러났다. ros2.sources가 심볼릭 링크라서 grep -r은 이 파일을 건너뛰고 0개를 보고했다. GNU grep 매뉴얼(grep manual)대로 -r은 재귀 중 만나는 심볼릭 링크를 따라가지 않고, -R은 따라간다.

직접 재현하려면 결과를 낸 커밋(7aaca06)에 고정된 experiment.py를 현재 디렉터리에 받은 뒤 같은 이미지 digest와 같은 조건으로 실행한다. 5회 원본 출력은 results.json, 예측과 시행착오 기록은 plan.md에 있으며, 전체 묶음은 실험 디렉터리에서 볼 수 있다. Docker가 설치된 Linux 기준이며 네트워크 없이 약 20초 걸린다.

curl -fsSLO https://raw.githubusercontent.com/sungpyo9053/blog/7aaca060ed2d095eb57b2a04c6decdde90aaf17d/experiments/ros2-apt-update-signature-invalid-ros2-apt-source/experiment.py
docker run --rm --network none --cpus 1 --memory 1g -v "$PWD":/work ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca bash -c "source /opt/ros/jazzy/setup.bash && python3 /work/experiment.py"

정상이라면 JSON 줄이 차례로 출력되고 마지막 줄이 "part": "done"이다. 키 ID와 t 값은 실행마다 다르지만, 종료 코드와 사람 손 횟수는 위 표와 같아야 한다.

실험 3부 타임라인: A 3대·B 3대·B_off 1대가 수동 설정으로 출발해 연장 1회차에서 A는 사람이 키 파일을 덮어쓰고 B는 apt로 새 키링을 자동 수신하며, 꺼져 있던 B_off만 만료 후 EXPKEYSIG(종료 코드 0)로 복구가 필요했고 연장 2회차에서 7대 모두 정상으로 돌아오는 과정과 사람 손 합계(A 6·B 3·B_off 2)를 보여주는 단계별 다이어그램

이 재현의 범위도 분명히 해 둔다. 실제 packages.ros.org, 실제 ROS 키(RSA 4096), HTTP 전송·미러·프록시는 재현하지 않았다. 이미지에 apt-ftparchive가 없어서 실험 저장소의 Packages·Release 색인은 Python으로 직접 썼다. “패키지 방식”의 갱신은 apt-get download로 후보 버전을 받아 dpkg-deb -x로 키링만 푸는 흉내이고, 실제 apt upgrade·unattended-upgrades의 도착 시점과 dpkg 등록은 재현하지 않았다. 만료 주기를 초 단위로 압축했으며, 사람 손 1회에 드는 시간이나 실수 확률은 재지 않았다.

내 기계에서 바꾸고 확인할 순서

아직 오류가 나지 않은 기계, 즉 키 사본이 유효한 기계는 이전 가이드의 순서를 따른다. 예전 설정으로 sudo apt update && sudo apt install ros2-apt-source를 실행하고, 곧바로 예전 파일 두 개를 지운 뒤 다시 확인한다. 패키지 설치와 삭제 사이에는 예전 파일과 새 파일이 공존한다. 이전 가이드가 키 충돌로 apt가 깨진다고 경고한 바로 그 상태이고, 실험에서도 이 조합의 apt-cache policy가 종료 코드 100으로 멈췄다. 삭제를 미루지 않는 편이 좋다.

sudo apt update && sudo apt install ros2-apt-source
sudo rm /etc/apt/sources.list.d/ros2.list
sudo rm /usr/share/keyrings/ros-archive-keyring.gpg
sudo apt update

이미 EXPKEYSIG가 나는 기계에 대해 이전 가이드는 두 길을 제시한다. 하나는 키를 수동으로 다시 받은 뒤 위 순서를 밟는 것이다. 2021년 채택 답의 ROS 2용 명령이 이 수동 갱신이다.

sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg

다른 하나는 예전 파일 두 개를 먼저 지우고 현재 설치 문서의 ros2-apt-source 단계(L83–L95)를 그대로 따르는 것이다. 이 경로는 .deb을 GitHub에서 HTTPS로 받으므로 ROS apt 저장소의 서명 검사를 거치지 않는다. 예전 파일을 지운 뒤에는 apt update가 ROS 저장소를 아예 읽지 않으니, 앞에서 본 && 중단도 피할 수 있다. 이 판단은 가이드의 두 번째 길과 설치 문서 단계를 묶어 해석한 것이고, 실제 서버를 상대로 실행해 본 결과는 아니다. 같은 질문의 다른 답에 있는 http://repo.ros2.org/repos.key | sudo apt-key add -는 쓰지 않는다. 키를 HTTP로 받는다는 점을 질문자가 직접 지적했고, apt-key는 폐기 예정인 방식이다.

옮긴 뒤에는 아래 명령으로 상태를 읽는다. 모두 읽기 전용 점검 예시이며, 출력은 기계마다 다르므로 예상 출력을 붙이지 않았다.

ls -l /etc/apt/sources.list.d/
grep -Rl packages.ros.org /etc/apt/sources.list.d/
dpkg -S /etc/apt/sources.list.d/ros2.sources
dpkg-query -W ros2-apt-source
gpg --show-keys /usr/share/keyrings/ros2-archive-keyring.gpg
sudo apt update

확인할 것은 다섯 가지다.

  1. ls -l 결과에서 ROS 관련 파일로 ros2.list는 사라지고 ros2.sources -> /usr/share/ros-apt-source/ros2.sources 링크만 남았는가.
  2. grep -Rl이 ROS 저장소 파일을 하나만 찾는가. -r을 쓰면 링크를 건너뛰어 0개로 보일 수 있다. 예전 튜토리얼과 달리 /etc/apt/sources.list 본문에 ROS 줄을 직접 넣었다면 그 파일도 같이 본다.
  3. dpkg -S가 소유 패키지를 ros2-apt-source로 보고하고, dpkg-query가 설치 버전을 보여 주는가.
  4. gpg --show-keys의 지문이 C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654이고 만료일이 2030-06-01인가.
  5. sudo apt update 출력에 EXPKEYSIG, Conflicting values set for option Signed-By, configured multiple times가 없는가. 종료 코드가 0이어도 W: 줄은 따로 읽는다. 갱신을 놓친 기계는 경고만 남기고 예전 색인으로 계속 동작한다.

로봇 PC라면 순서가 하나 더 붙는다. apt upgrade는 ROS 저장소의 드라이버와 노드 라이브러리도 함께 올릴 수 있다. 로봇이 움직이는 중에는 업그레이드하지 말고, 정지 또는 비상정지 상태로 둔 뒤 진행하고, 끝나면 노드를 다시 띄워 동작을 확인한다. 이 안내는 일반적인 운영 판단이며 이 글의 실험으로 검증한 내용은 아니다. Docker 이미지에서는 다른 함정이 있다. 2021년 채택 답이 짚었듯, 키를 받는 명령 문자열이 같으면 레이어 캐시 때문에 다시 실행되지 않아 만료된 사본이 이미지에 남을 수 있다. 측정한 ros:jazzy-ros-base digest에는 예전 파일이 없었지만, 예전 수동 단계를 직접 적어 둔 Dockerfile은 따로 확인해야 한다.

지금 옮길 기계와 그대로 둘 기계

새로 설치하는 기계는 고민할 이유가 적다. 어차피 초기 설정이 한 번 필요하고, 현재 공식 문서도 이 경로를 쓴다. 판단이 필요한 쪽은 이미 수동 방식으로 잘 돌아가는 기계다. 2030년 연장 한 번만 겪고 교체할 기계라면 횟수상 이득이 없다(k = 1). 그 뒤로도 쓸 기계, 그리고 평소 업데이트를 꾸준히 받는 기계라면 옮겨 둘 만하다. 반대로 창고에 오래 꺼져 있는 장비가 많다면 m이 커지므로, 이전 자체보다 만료일 전에 장비를 켜서 업데이트하는 일정을 먼저 잡아야 한다. 사설 미러나 오프라인 저장소를 쓰는 환경은 이 글의 범위 밖이다. 이전 가이드의 FAQ는 이 경우 패키지 파일을 복사해 URL을 바꾼 별도 소스와 apt 우선순위 설정으로 구성할 수 있다고만 안내한다.

어느 쪽을 고르든 남는 불확실성이 있다. 패키지 경로로 키가 실제로 갱신된 사례는 아직 없고, 2030년 연장 때 새 패키지가 만료 전에 얼마나 일찍 배포될지는 미래의 일이다. 그래서 키 만료일을 직접 읽고, apt update의 W: 줄을 확인하는 습관이 어떤 방식에서든 가장 확실한 안전장치로 남는다. 자기 기계의 /etc/apt/sources.list.d 목록과 키 만료일을 확인한 뒤, ROS 2 공식 Ubuntu 설치 문서의 ros2-apt-source 단계와 서명 키 이전 가이드를 이어서 읽으면 된다.

비슷한 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다