systemd 자격 증명으로 서비스 비밀값을 격리하고 키 교체 검증하기

systemd 자격 증명으로 서비스 비밀값을 격리하고 키 교체 검증하기

한 대의 물리 서버에서 API 서비스, 동기화 작업, 모니터링 수집기를 함께 실행하다 보면 서비스 토큰이 시작 스크립트, 환경 설정 파일, 배포 기록 곳곳에 흩어지기 쉽습니다. 프로세스 인자를 확인하거나 장애 로그를 남기는 과정, 또는 설정을 한 번 보관하는 작업만으로도 노출 범위가 넓어질 수 있습니다. 이 글에서는 Debian 12에서 systemd의 LoadCredential을 사용해 키 원본 파일을 관리자가 통제하는 디렉터리에 보관하고, 지정한 서비스에만 런타임 사본을 전달합니다. 이어서 반복 가능한 검사로 키 교체 절차를 검증합니다.

1. 자격 증명 격리의 경계부터 명확히 하기

여기서 다루는 문제는 “서비스가 비밀값을 어떻게 전달받는가”입니다. 키 발급, 중앙 보관, 원격 인증 프로토콜을 다루지는 않습니다. CloudDDR 물리 서버의 시스템과 데이터는 사용자가 직접 관리합니다. 결제 후 콘솔에서 접속 자격 증명을 확인하고 SSH로 시스템에 접속합니다. 아래 작업에는 관리자 권한이 필요하며, 먼저 테스트 서비스에서 수행해야 합니다. 운영 서비스의 시작 방식부터 바로 변경하지 마세요.

환경 변수는 비밀값을 전달하는 기본 수단으로 적합하지 않습니다. 디버그 출력, 자식 프로세스의 환경, 진단 자료 아카이브에 포함될 수 있기 때문입니다. 시작 인자 역시 프로세스 검사 도구에 기록될 수 있습니다. 반면 LoadCredential은 시스템 서비스 관리자가 원본 파일을 읽고 서비스 시작 시 자격 증명 디렉터리를 제공합니다. 프로그램은 CREDENTIALS_DIRECTORY를 통해 해당 파일을 찾습니다.

자격 증명 파일을 격리해도 이미 관리자 권한을 확보한 프로세스를 막을 수는 없습니다. 애플리케이션이 비밀값을 로그에 쓰는 것도 방지하지 못합니다. 보호 범위에는 파일 권한, 서비스 계정, 애플리케이션의 출력 동작이 모두 포함되어야 합니다.

이 예제의 원본 파일은 여전히 평문입니다. LoadCredential을 사용한다고 해서 저장 상태의 암호화가 자동으로 적용되지는 않습니다. 서버 전체 백업, 관리자 접근, 키 발급 기록도 민감한 데이터에 준해 관리해야 합니다.

2. 전용 계정과 접근이 제한된 원본 파일 만들기

독립된 검사 환경 준비하기

systemctl --version으로 버전을 확인합니다. 이 예제에서는 Debian 12가 제공하는 systemd를 사용하고 Python 3를 설치합니다. 계정 이름이 기존 서비스에서 사용 중인지 확인하세요. 아래 계정 생성 명령은 해당 계정이 아직 없을 때만 실행합니다.

sudo apt-get update
sudo apt-get install python3
id credential-check
sudo useradd --system --no-create-home --shell /usr/sbin/nologin credential-check
sudo install -d -o root -g root -m 0700 /etc/service-secrets
sudo sh -c 'umask 077; printf "%s" "fixture-token-alpha" > /etc/service-secrets/api-token'
sudo stat -c '%U %G %a %n' /etc/service-secrets /etc/service-secrets/api-token

id 결과에서 계정이 없음을 확인한 뒤에만 useradd를 실행합니다. 이미 존재한다면 용도를 확인해야 하며, 용도를 모르는 계정을 그대로 재사용해서는 안 됩니다. 디렉터리 권한은 700, 파일 권한은 600, 소유자와 소유 그룹은 모두 root여야 합니다. 예제 문자열은 아무 권한도 부여하지 않는 테스트 데이터이므로 명령 기록에 남아도 됩니다. 실제 토큰은 이런 방식으로 명령에 직접 넣지 마세요.

실제 값은 통제된 파일 전달 절차로 받아 관리자 권한으로 대상 위치에 설치해야 합니다. 편집기의 임시 파일, 터미널 에코, 추적 기능이 켜진 Shell을 통해 비밀값을 전달하지 마세요. 서비스 계정은 원본 파일을 읽을 수 없어야 합니다.

sudo -u credential-check test -r /etc/service-secrets/api-token
printf '%s\n' "$?"

예상 종료 코드는 1입니다. 0이라면 디렉터리 권한, 파일 ACL, 계정의 그룹 소속부터 확인하고 배포를 진행하지 마세요.

3. 프로그램이 런타임 자격 증명만 읽도록 구성하기

상위 디렉터리를 만든 뒤 다음 프로그램을 /usr/local/libexec/credential-check.py에 저장하고, 소유자는 root, 권한은 755로 설정합니다. 디렉터리와 프로그램 모두 서비스 계정에 쓰기 권한을 허용해서는 안 됩니다. 프로그램은 정해진 오류 메시지만 출력하며 토큰, 길이, 다이제스트는 출력하지 않습니다. 검증을 위해 또 다른 유출 경로를 만드는 일을 피하기 위한 조치입니다.

import os
from pathlib import Path

directory = os.environ.get("CREDENTIALS_DIRECTORY")
if not directory:
    raise SystemExit("Credential directory missing")

try:
    token = (Path(directory) / "api-token").read_bytes()
except OSError:
    raise SystemExit("Credential unavailable") from None

if not token or b"\n" in token or b"\r" in token:
    raise SystemExit("Credential format rejected")

이 예제에서는 토큰이 비어 있지 않고 줄바꿈 문자를 포함하지 않아야 한다고 정했습니다. 모든 키 형식에 적용되는 규칙은 아닙니다. 실제 프로그램은 연동 대상의 프로토콜에 맞춰 검증해야 합니다. 예를 들어 여러 줄의 PEM 내용을 허용한다면 이 검사를 그대로 사용할 수 없습니다.

/etc/systemd/system/credential-check.service를 만듭니다.

[Unit]
Description=Service credential validation

[Service]
Type=oneshot
User=credential-check
Group=credential-check
LoadCredential=api-token:/etc/service-secrets/api-token
ExecStart=/usr/bin/python3 /usr/local/libexec/credential-check.py
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
UMask=0077

일회성 검사 유닛이므로 성공한 뒤 inactive 상태가 되는 것은 정상입니다. 부팅 시 자동 시작하도록 설정할 필요가 없으며, 테스트를 위해 운영 서비스의 재시작 정책을 적용해서도 안 됩니다. 실제 애플리케이션도 같은 방식으로 파일을 읽을 수 있지만, 파일 시스템 제한은 서비스가 데이터를 쓰는 디렉터리를 고려해 검증해야 합니다. 설정을 무작정 복사하지 마세요.

4. 읽기 가능 여부와 인증 결과를 각각 검증하기

sudo systemctl daemon-reload
sudo systemctl start credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus
sudo journalctl -u credential-check.service -n 20 --no-pager

예상 결과는 Result=success, ExecMainStatus=0입니다. 이는 서비스 계정이 런타임 디렉터리를 통해 자격 증명을 읽을 수 있다는 뜻입니다. 앞서 원본 파일은 직접 읽을 수 없다는 점도 확인했습니다. 이를 증명하려고 cat, 전체 환경 변수 출력, 프로세스 메모리 덤프를 사용하지 마세요.

이 검사만으로 토큰의 유효성까지 확인되는 것은 아닙니다. 실제 서비스에 연결한 뒤에는 업무 데이터를 변경하지 않는 인증 요청을 실행하고, 응답 상태와 예상 권한을 확인해야 합니다. 인증에 실패하면 자격 증명 형식, 연동 대상의 새 토큰 활성화 여부, 애플리케이션의 실제 재시작 여부, 요청이 이전 연결이나 캐시를 사용했는지를 각각 확인합니다.

검증 기록에는 유닛 버전, 작업 순서, 종료 코드, 인증 결과만 남기고 토큰 값은 저장하지 않습니다. 특정 교체 작업과 기록을 연결해야 한다면 토큰 다이제스트 대신 내부 변경 번호를 사용하세요. 로그를 확인할 때는 예외 처리 경로에서 요청 헤더나 전체 설정을 출력하지 않는지도 살펴야 합니다.

5. 원자적 교체로 키를 변경하고 실패 시 복구하기

원본 파일을 갱신해도 실행 중인 자격 증명 사본은 바뀌지 않습니다. 키 교체는 새 값 확보, 형식 검증, 원자적 교체, 서비스 재시작, 인증 검증 순서로 진행하고, 마지막에 연동 대상의 규칙에 따라 이전 값을 폐기해야 합니다. daemon-reload만 실행해서는 실행 중인 비밀값이 갱신되지 않습니다.

다음 명령은 테스트 데이터만 교체합니다. 임시 파일과 대상 파일을 같은 디렉터리에 두어 이름 변경이 동일한 파일 시스템 안에서 이루어지도록 합니다.

sudo sh -c 'umask 077; printf "%s" "fixture-token-beta" > /etc/service-secrets/api-token.next'
sudo chown root:root /etc/service-secrets/api-token.next
sudo chmod 0600 /etc/service-secrets/api-token.next
sudo mv -f /etc/service-secrets/api-token.next /etc/service-secrets/api-token
sudo systemctl restart credential-check.service
sudo systemctl show credential-check.service -p Result -p ExecMainStatus

이어서 실패 상황을 시험합니다. 테스트 원본 파일을 빈 파일로 교체한 뒤 검사 유닛을 다시 시작하면, 0이 아닌 종료 코드와 정해진 형식 오류 메시지가 나와야 합니다. 비어 있지 않은 테스트 값으로 복원한 뒤 다시 시작하면 성공해야 합니다. 이렇게 하면 비밀값이 없을 때 애플리케이션이 아무 경고 없이 계속 실행되지 않는지 확인할 수 있습니다. 운영 토큰을 대상으로 이런 파괴적 테스트를 수행하지 마세요.

실제 교체 전에는 연동 대상이 새 토큰과 이전 토큰을 동시에 허용하는지 확인해야 합니다. 허용하지 않는다면 양쪽의 전환을 조율하는 절차가 필요합니다. 이전 값이 이미 폐기되었다면 이전 파일을 복원해도 인증은 복구되지 않습니다. 이전 값은 통제된 롤백 기간 동안만 보관하고 동일한 수준의 권한 보호를 적용해야 합니다. 서비스 디렉터리에 복사해 장기간 남겨 두어서는 안 됩니다.

검증을 마치면 테스트 유닛을 비활성화하고 유닛, 스크립트, 테스트 자격 증명을 제거합니다. daemon-reload를 실행한 뒤 임시 파일이 남아 있지 않은지도 확인합니다. 최종 결과물은 다시 검토할 수 있는 절차여야 합니다. 일반 계정은 원본 파일을 읽을 수 없고, 대상 서비스는 런타임 자격 증명을 읽을 수 있으며, 잘못된 값은 명확한 실패로 이어져야 합니다. 또한 교체 후 실제 인증이 성공하고 로그에는 민감한 값이 없어야 합니다.

자주 묻는 질문

LoadCredential로 관리자에게도 비밀값을 숨길 수 있나요?

없습니다. 관리자와 충분한 권한을 가진 프로세스는 원본 파일이나 서비스 메모리를 읽을 수 있습니다. 실행 인자, 환경 변수, 일반 계정을 통한 노출을 줄이는 기능이며 관리자에 대한 기밀성 경계는 아닙니다.

원본 파일을 교체하면 실행 중인 서비스에도 바로 반영되나요?

아닙니다. LoadCredential은 서비스 시작 시 자격 증명을 읽습니다. 원본 파일을 원자적으로 교체한 뒤 서비스를 재시작하고 새 비밀값으로 인증 결과를 확인해야 합니다. daemon-reload만 실행해서는 갱신되지 않습니다.

AMD EPYC 7543P 단독 사용 물리 서버, 월 $447부터.

물리 서버 구성은 메모리부터 선택하세요

AMD EPYC 7543P, 128GB 및 256GB DDR4-3200 ECC의 두 가지 구성.