Files
document-haness/docs/virtualization/tech-log-studio/build-completion-judgment/case/case-an-empty-token-installed-the-agent-anyway.md
T

10 KiB

kind, slug, title, topic, topicName, project, status, sourceRevision, evidence, source
kind slug title topic topicName project status sourceRevision evidence source
CASE an-empty-token-installed-the-agent-anyway 빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다 build-completion-judgment 끝났다는 판정 virtualization 게시 전 import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
../../../final/evidence/raw/lab-state-before-rebuild/15-k3s-precheck.txt
../../../final/evidence/raw/lab-state-before-rebuild/21-k3s-token.txt
../../../final/evidence/raw/lab-state-before-rebuild/22-k3s-agent-install.txt
final/document.md#185-가이드-묶음이-스스로-정한-규약
final/document.md#188-단계-02-k3s-server-와-agent
final/document.md#184-이-부의-출처와-범위

빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다

k3s agent 설치는 오류 한 줄 없이 끝났는데 kubectl get nodes 에는 노드가 하나뿐이었다. 게스트 안에서 친 ssh 가 실패해 토큰이 빈 문자열로 넘어갔고, 설치 스크립트는 그 전까지를 성공으로 찍었다. 값을 찍지 않고 길이만 재는 한 줄을 설치 앞에 두면 잡힌다.

관계

  • 도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다 설치 스크립트의 출력을 노드가 붙었다는 뜻으로 읽은 사건이라 그 규칙의 사례가 된다.
  • 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다 설치가 끝났다는 출력 대신 각 층에서 무엇이 성공 신호인지로 판정을 옮기는 절차다.
  • cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다 같은 구축의 한 단계 앞에서 벌어진 일이고, 거기서도 실패가 아무 오류 없이 지나갔다.
  • 가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가 여기 적은 설치 명령을 다시 쳐서 같은 상태가 되는지는 확인된 적이 없다.
  • 단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다 명령을 어느 셸에서 치는지를 단계마다 확인하라는 규칙이고, 그 확인이 빠졌을 때 나온 실패가 이것이다.

문제

k3s agent 설치가 끝까지 돌았고 출력에 오류가 없었다. 그런데 두 노드가 Ready 로 나와야 할 kubectl get nodes 에 두 번째 노드가 나타나지 않았다.

설치 출력의 오류 : x kubectl get nodes 의 노드 수 : 1 k3s-agent 유닛 : 5초마다 재시작 journalctl -u k3s-agent : level=fatal msg="Error: --token is required"

실패가 화면에 나오지 않아 설치는 끝난 것으로 읽힌다.

결론

토큰을 꺼내는 ssh 를 게스트 안에서 쳤고 그 명령이 Host key verification failed. 로 끝났다. 명령 치환으로 감싸면 오류는 stderr 로 흘러가고 변수에는 빈 문자열이 담긴다. 셸은 아무 불평도 하지 않는다.

agent 는 --token '' 을 받아 level=fatal msg="Error: --token is required" 로 죽지만, 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝난다. 그래서 설치 출력만 보면 성공이고, 유닛이 Restart=always 라 5초마다 조용히 재시도한다.

해결 : 게스트에 들어가지 않고 lab host 한 셸에서 ssh kc-lab-1 '...' 형태로 친다 가드 : 값을 쓰기 전에 길이로 가른다. 이 실험대의 토큰은 108자였다 히스토리에 남기지 않으려면 : --token-file 로 넘긴다. 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다

검증 환경

호스트 : test-server, Arch Linux CPU : i5-1135G7, 논리 코어 8 RAM : 11,648MiB QEMU : 11.1.1 libvirt : 12.7.0 게스트 : Debian 12 genericcloud k3s : v1.36.4+k3s1 server 노드 : kc-lab-1, 192.168.122.11 agent 노드 : kc-lab-2, 192.168.122.12 토큰 길이 : 108자 토큰 형식 : K10<해시>::server:<비밀번호>

재현 조건

  1. lab host 에서 게스트 세 대를 세우고 kc-lab-1 에 k3s server 를 깐다.
  2. lab host 에서 ssh kc-lab-1 로 게스트에 로그인한다.
  3. 게스트 프롬프트에서 TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token') 을 친다.
  4. echo "${#TOKEN} 자" 가 0 을 낸다.
  5. 같은 셸에서 agent 설치 명령에 --token "$TOKEN" 을 넘긴다. 설치 출력은 오류 없이 끝난다.
  6. lab host 로 돌아와 kubectl get nodes 를 친다. 노드가 하나뿐이다.
  7. ssh kc-lab-2 'journalctl -u k3s-agent' 에서 --token is required 를 찾는다.

본문

토큰이 지나는 길과 명령을 치는 셸

k3s server 는 kc-lab-1 에, agent 는 kc-lab-2 에 깔고 둘 다 lab host 에서 ssh 로 원격 실행한다. 가이드가 코드 블록마다 어느 기계에서 치는지를 붙여 둔 까닭이 여기에 있다 — 게스트는 libvirt NAT(192.168.122.0/24) 안에 있어 워크스테이션에서 직접 닿지 않고, ssh kc-lab-1 이라는 별칭도 lab host 의 ~/.ssh/config 에만 있다.

agent 가 server 에 붙으려면 server 가 만든 node-token 이 필요하고, 그 값을 lab host 에서 꺼내 같은 셸에서 설치 명령에 넘긴다.

TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token')
echo "${#TOKEN} 자"        # 값이 아니라 길이만 확인한다

비밀은 길이나 존재 여부만 확인하고 값을 찍지 않는다. 가이드가 먼저 정한 표기 규약이고, 값을 찍으면 터미널 스크롤백과 화면 공유에 그대로 남기 때문이다.

ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
  --server https://192.168.122.11:6443 \
  --token '$TOKEN' \
  --node-ip 192.168.122.12"

게스트 안에서 친 ssh 는 빈 문자열이 된다

토큰을 꺼내려고 게스트에 먼저 들어가면 같은 명령이 다르게 끝난다. 게스트에는 lab host 의 개인키도 ~/.ssh/config 도 없다.

[kc-lab-1] $ ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token'
Host key verification failed.

여기까지는 오류 문구가 찍힌다. 문제는 이 명령을 TOKEN=$(...) 로 감쌌을 때다. 명령 치환은 표준 출력만 변수에 담으므로 오류는 stderr 로 흘러가고 TOKEN 에는 빈 문자열이 담기며, 셸은 아무 불평도 하지 않는다. 뒤이어 치는 설치 명령은 --token '' 을 넘긴 것과 같아진다.

설치 출력이 성공으로 끝나는 경로

빈 문자열을 받은 설치도 끝까지 돈다. agent 가 --token '' 을 받아 level=fatal msg="Error: --token is required" 로 죽는데, 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝나므로 설치 출력만 보면 성공이다. 성공으로 찍힌 것은 내려받기와 유닛 생성과 enable 까지다. 유닛은 Restart=always 라 5초마다 조용히 재시도하고, 그래서 실패가 화면이 아니라 journalctl -u k3s-agent 안에서만 되풀이된다.

k3s server 와 agent 를 세우는 단계가 끝났다는 판정은 lab host 에서 노드 목록을 쳐서 두 노드가 Ready 로 나오는 것이다.

kubectl get nodes -o wide
NAME       STATUS   ROLES           AGE   VERSION        INTERNAL-IP
kc-lab-1   Ready    control-plane   47m   v1.36.4+k3s1   192.168.122.11
kc-lab-2   Ready    <none>          21m   v1.36.4+k3s1   192.168.122.12

빈 토큰으로 깔린 agent 는 이 목록에 올라오지 않는다.

값을 찍지 않고 길이만 잰다

토큰을 화면에 찍어 눈으로 대조하는 방법은 표기 규약이 막아 두었다. 그래서 설치 앞에 길이를 재는 한 줄을 둔다.

[ ${#TOKEN} -ge 50 ] || echo "TOKEN 이 비었다 — 3번으로 돌아간다"

이 실험대의 토큰은 108자였고 형식이 K10<해시>::server:<비밀번호> 라 k3s 판올림에 따라 자릿수가 달라진다. 그래서 이 가드는 값을 맞춰 보지 않고 길이가 0 이 아닌지만 본다.

길이가 0 으로 나오는 길은 셋이다. 가이드는 게스트 안에서 친 경우를 그중 가장 흔하다고 적어 두었고, 나머지 둘은 server 가 아직 안 떠서 토큰 파일이 없거나 새 셸을 열어 변수가 사라진 경우를 가리킨다. 가드 한 줄은 그 몇 분을 막는다 — 설치가 성공으로 끝나고, 노드 목록에서 한 줄이 빠진 것을 알아채고, journalctl 까지 가는 동안이다.

토큰을 파일로 넘기면 같은 셸 제약이 없어진다

위 가드는 TOKEN 이 셸 변수라 토큰을 꺼낸 셸과 같은 셸에서 쳐야 하고, 다른 창에서는 비어 있다. 토큰이 명령줄에 들어가는 것도 걸리면 파일로 넘긴다.

ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' \
  | ssh kc-lab-2 'sudo tee /tmp/token >/dev/null'
ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
  --server https://192.168.122.11:6443 --token-file /tmp/token \
  --node-ip 192.168.122.12; rm -f /tmp/token"

토큰이 셸 히스토리에 남지 않고, 변수를 쓰지 않으니 셸이 달라도 된다.

확인하지 못한 것

원래 빈 토큰 사건의 level=fatal msg="Error: --token is required" 와 당시 설치 출력은 여전히 직접 raw가 없다. 다만 2026-09-17 후속 원문은 현재 정상 경로를 보강한다. 15-k3s-precheck.txt 에는 별도 precheck에서 Host key verification failed.가 남아 있고, 21-k3s-token.txt 는 재구축 시점의 비어 있지 않은 토큰을 길이로 확인했으며, 22-k3s-agent-install.txt 는 그 토큰으로 agent 설치가 성공한 출력을 담는다. 이 셋은 원래 빈 토큰 실패의 재현이 아니라 follow-up corroboration이다. 과거 사건의 토큰 108자와 후속 실행의 길이가 다르더라도 같은 측정으로 덮어쓰지 않는다.

이 실패를 일부러 다시 만들어 본 기록도 없어서, 빈 토큰으로 설치하면 설치 출력이 성공으로 끝난다는 것은 한 번의 관측이다. 재현이 쉽지 않은 까닭은 이 부의 검증 방식에 있다 — 만드는 명령은 다시 치면 돌고 있는 실험대가 없어지므로 구축할 때 쓴 것을 옮기고 결과 상태를 확인하는 것으로 대신했다. 재현 없이 남길 수 있는 것은 journalctl 쪽이고, 그 출력을 final/evidence/raw/ 에 남기면 진단 절에 증거가 붙는다.

가드 한 줄이 실제로 빈 토큰을 잡아 본 기록도 없다.