콘솔 복구와 sudo, 전용 <RUNNER_USER>·고유한 <RUNNER_NAME>
GITHUB ACTIONS
GitHub Actions Self-hosted Runner 안전 구성
승인된 Linux runner를 전용 계정·runner group·명시적 label로 등록하고 서비스 운영, 업데이트, 제거와 무데이터 롤백까지 안전하게 구성합니다.BEFORE YOU START
시작 전에 준비하세요
승인된 repository 또는 organization URL과 제한된 runner group
릴리스 자산의 정확한 version·architecture·공식 SHA-256
등록 직전에 발급하고 로그·history에 남기지 않을 1시간 유효 registration token
권장 대상 GitHub Actions 작업을 사설 인프라에서 실행하며 runner 격리·권한·수명주기를 책임지는 운영자
FOLLOW THE RECIPE
8단계 구성·점검 레시피
범위·호스트·신뢰 경계 확인
runner가 접근할 repository와 secret, 네트워크, 기존 CI process를 읽기 전용으로 확인합니다.
cat /etc/os-release
uname -mgetent passwd <RUNNER_USER>
id <RUNNER_USER>
free -h
df -hT / /srvsystemctl list-unit-files 'actions.runner.*' --no-pager
ps -eo pid,user,etime,comm,args | grep '[R]unner.Listener'- repository/organization 관리자 승인과 runner group 범위를 기록했습니다.
- 비신뢰 fork PR은 이 지속형 runner에서 실행하지 않습니다.
- host가 다른 업무·credential과 공유되지 않습니다.
공유 host나 광범위한 secret이 보이면 전용 VM 또는 일회성 runner 설계가 먼저입니다.
공식 릴리스·네트워크·기존 등록 충돌을 확인합니다.
릴리스·네트워크·등록 충돌 사전 점검
GitHub 설정 화면이 제시한 릴리스와 checksum을 변경 기록에 고정하고 outbound 연결과 기존 service를 확인합니다.
systemctl status 'actions.runner.<SCOPE>.<RUNNER_NAME>.service' --no-pager -l || truesudo test -d /srv/actions-runner && sudo find /srv/actions-runner -maxdepth 1 -type f -printf '%m %u:%g %f
' || trueif sudo test -x /srv/actions-runner/config.sh; then sudo -u <RUNNER_USER> -H bash -lc 'cd /srv/actions-runner && read -rsp "Diagnostic token: " CHECK_PAT && printf "\n" && ./config.sh --check --url https://github.com/<OWNER>/<REPOSITORY> --pat "$CHECK_PAT" && unset CHECK_PAT'; fi신규 설치는 package 설치 뒤 동일한 진단을 수행합니다. 권한을 최소화한 단기 자격증명을 history·출력에 남기지 않습니다.- GitHub 설정 화면의 권장 runner version과 progressive rollout 상태를 확인했습니다.
- 동일 name·work directory·service 등록이 없습니다.
- 필수 GitHub endpoint만 outbound allowlist에 포함됩니다.
토큰을 명령행·환경 덤프로 노출했거나 기존 등록 소유자가 불명확하면 중단합니다.
0700 작업공간에서 검증한 package를 설치합니다.
고정 runner package 검증·설치
공식 릴리스 자산을 임시 전용 경로에 내려받아 SHA-256을 대조한 뒤 runner 전용 계정 소유 경로에 풉니다.
getent group <RUNNER_GROUP> >/dev/null || sudo groupadd --system <RUNNER_GROUP>
getent passwd <RUNNER_USER> >/dev/null || sudo useradd --system --create-home --home-dir /home/<RUNNER_USER> --shell /usr/sbin/nologin --gid <RUNNER_GROUP> <RUNNER_USER>
id <RUNNER_USER>
test "$(id -gn <RUNNER_USER>)" = '<RUNNER_GROUP>'
getent passwd <RUNNER_USER>
getent group <RUNNER_GROUP>기존 계정도 primary group이 <RUNNER_GROUP>인지 fail-fast 검증합니다. nologin이므로 sudo -u <RUNNER_USER> -H bash -lc를 사용합니다.umask 077
mktemp -d -p /var/tmp haru-actions-runner.XXXXXXXX출력 경로를 <RUNNER_WORK_DIR>에 그대로 대입합니다.stat -Lc '%a %U:%G %n' <RUNNER_WORK_DIR>
curl -fL -o <RUNNER_WORK_DIR>/runner.tar.gz https://github.com/actions/runner/releases/download/v<APPROVED_RUNNER_VERSION>/actions-runner-linux-<RUNNER_ARCH>-<APPROVED_RUNNER_VERSION>.tar.gz
printf '%s %s
' '<APPROVED_RUNNER_SHA256>' '<RUNNER_WORK_DIR>/runner.tar.gz' | sha256sum --check --strictsudo install -d -o <RUNNER_USER> -g <RUNNER_GROUP> -m 0750 /srv/actions-runner
sudo tar --no-same-owner -xzf <RUNNER_WORK_DIR>/runner.tar.gz -C /srv/actions-runner
sudo chown -R <RUNNER_USER>:<RUNNER_GROUP> /srv/actions-runner
sudo -u <RUNNER_USER> /srv/actions-runner/bin/Runner.Listener --version- version·architecture·SHA-256이 승인값과 일치합니다.
- runner는 root가 아닌 전용 계정 소유입니다.
- 다운로드 URL은 actions/runner 공식 릴리스입니다.
checksum이 다르면 압축을 풀지 않고 작업공간을 격리합니다.
단기 registration token으로 제한된 group·label에 등록합니다.
등록·group·label·service 구성
등록 토큰은 발급 후 1시간 안에 한 번만 사용하고 값은 붙여넣기 입력합니다. 조직 runner는 승인된 runner group으로 제한합니다.
sudo install -d -o root -g root -m 0700 <APPROVED_BACKUP_DIR>
if sudo test -f /srv/actions-runner/.runner; then sudo cp --archive --no-clobber /srv/actions-runner/.runner <APPROVED_BACKUP_DIR>/.runner.<BACKUP_SUFFIX>; sudo chmod 0600 <APPROVED_BACKUP_DIR>/.runner.<BACKUP_SUFFIX>; fi
if sudo test -f /srv/actions-runner/.service; then sudo cp --archive --no-clobber /srv/actions-runner/.service <APPROVED_BACKUP_DIR>/.service.<BACKUP_SUFFIX>; sudo chmod 0600 <APPROVED_BACKUP_DIR>/.service.<BACKUP_SUFFIX>; fi
if sudo test -f /srv/actions-runner/.credentials; then sudo stat -Lc '%a %U:%G %s %n' /srv/actions-runner/.credentials; sudo sha256sum /srv/actions-runner/.credentials; fi.credentials는 복사하지 않고 root 화면에 metadata·checksum만 증거로 기록합니다. .runner·.service 사본은 운영 경로로 복원하지 않습니다.sudo -u <RUNNER_USER> -H bash -lc 'cd /srv/actions-runner && read -rsp "Diagnostic token: " CHECK_PAT && printf "\n" && ./config.sh --check --url <APPROVED_RUNNER_SCOPE_URL> --pat "$CHECK_PAT" && unset CHECK_PAT'sudo -u <RUNNER_USER> -H bash -lc 'cd /srv/actions-runner && read -rsp "Registration token: " RUNNER_TOKEN && printf "\n" && ./config.sh --unattended --url https://github.com/<OWNER>/<REPOSITORY> --token "$RUNNER_TOKEN" --name <RUNNER_NAME> --labels <APPROVED_LABELS> --work _work && unset RUNNER_TOKEN'repository scope에서만 선택합니다. B와 동시에 실행하지 않습니다.sudo -u <RUNNER_USER> -H bash -lc 'cd /srv/actions-runner && read -rsp "Registration token: " RUNNER_TOKEN && printf "\n" && ./config.sh --unattended --url https://github.com/<OWNER> --token "$RUNNER_TOKEN" --runnergroup <APPROVED_RUNNER_GROUP> --name <RUNNER_NAME> --labels <APPROVED_LABELS> --work _work && unset RUNNER_TOKEN'organization scope에서만 선택합니다. A와 동시에 실행하지 않으며 repository runner에는 --runnergroup을 쓰지 않습니다.sudo -u <RUNNER_USER> test -f /srv/actions-runner/.runner
sudo stat -Lc '%a %U:%G %n' /srv/actions-runner /srv/actions-runner/.runnersudo bash -lc 'cd /srv/actions-runner && ./svc.sh install <RUNNER_USER>'
systemctl cat 'actions.runner.<SCOPE>.<RUNNER_NAME>.service'- token 값을 history·journal·ticket에 남기지 않았습니다.
- runner group은 선택한 repository만 허용합니다.
- label은 민감 capability를 명시하며 workflow가 self-hosted만 넓게 선택하지 않습니다.
- service User는 전용 비 root 계정입니다.
예상 group·label·scope와 다르면 job을 받기 전에 제거 토큰으로 등록을 해제합니다.
service를 시작하고 GitHub UI의 Idle 상태를 확인합니다.
service 시작·등록 상태 확인
service를 시작한 뒤 로컬 상태와 GitHub UI를 함께 확인하고 최소 권한 진단 workflow만 실행합니다.
sudo bash -lc 'cd /srv/actions-runner && ./svc.sh start && ./svc.sh status'systemctl status 'actions.runner.<SCOPE>.<RUNNER_NAME>.service' --no-pager -l
systemctl show 'actions.runner.<SCOPE>.<RUNNER_NAME>.service' -p User -p ActiveState -p NRestartsjournalctl -u 'actions.runner.<SCOPE>.<RUNNER_NAME>.service' -n 100 --no-pagerrepository 이름·경로·token·secret을 외부 공유 전에 마스킹합니다.- GitHub UI에서 runner가 Online·Idle입니다.
- 서비스 계정과 unit이 승인값입니다.
- 진단 workflow에 write token·production secret을 주지 않았습니다.
Offline·restart loop이면 job을 queue하지 않고 네트워크·권한·runner diagnostic log를 확인합니다.
업데이트·격리·업무 smoke test를 검증합니다.
업데이트 정책·업무 검증
runner 자동 업데이트 상태를 감시하고 조직 변경창에서 승인 버전을 검증합니다. 자동 업데이트를 끈 경우 GitHub의 30일 업데이트 요구를 운영 기준에 반영합니다.
sudo -u <RUNNER_USER> /srv/actions-runner/bin/Runner.Listener --version
systemctl is-enabled 'actions.runner.<SCOPE>.<RUNNER_NAME>.service'
systemctl is-active 'actions.runner.<SCOPE>.<RUNNER_NAME>.service'sudo find /srv/actions-runner/_diag -maxdepth 1 -type f -name 'Runner_*' -printf '%TY-%Tm-%TdT%TH:%TM %m %u:%g %p
' | tail -20로그 본문을 공유하기 전 secret·repository·내부 주소를 마스킹합니다.sudo find /srv/actions-runner/_work -mindepth 1 -maxdepth 2 -printf '%m %u:%g %p
' | head -100- 승인 version과 GitHub가 요구하는 최소 version을 충족합니다.
- ephemeral runner 로그는 host 밖의 승인된 로그 저장소로 전달됩니다.
- job 뒤 workspace·credential·process 격리를 확인했습니다.
지속형 runner는 작업 간 상태가 남을 수 있으므로 비신뢰 PR에는 사용하지 않습니다.
권한·secret·update·service hardening을 점검합니다.
비신뢰 코드·secret·host 권한 점검
self-hosted runner에서 실행된 코드는 host와 후속 job을 손상할 수 있다고 가정하고 trust boundary를 분리합니다.
id <RUNNER_USER>
sudo -l -U <RUNNER_USER>sudo namei -l /srv/actions-runner /srv/actions-runner/_worksudo ss -lntup
sudo ufw status verbose- fork PR과 외부 contributor 코드는 이 runner에 배정하지 않습니다.
- GITHUB_TOKEN permissions는 workflow별 read 최소값이며 secret은 environment 승인으로 보호합니다.
- runner 계정에 sudo·Docker socket·production SSH key를 주지 않습니다.
- 일회성 runner는 한 job 뒤 폐기하고 외부 로그를 보존합니다.
- <...> 자리표시자는 승인된 리터럴 값으로 직접 치환하며 외부 입력으로 shell 명령을 조립하거나 eval하지 않습니다.
sudo나 Docker socket, 비신뢰 workflow, 공유 production credential이 있으면 운영 투입을 중단합니다.
등록 해제와 backup/신규 설치 롤백을 분리합니다.
등록 해제·service 중지·신규 파일 격리
GitHub UI에서 job이 없음을 확인하고 단기 remove token을 대화형으로 입력해 먼저 등록을 해제합니다. package·work data를 삭제하지 않습니다.
sudo bash -lc 'cd /srv/actions-runner && ./svc.sh status'
sudo find /srv/actions-runner/_diag -maxdepth 1 -type f -printf '%m %u:%g %s %p
' | tail -20sudo bash -lc 'cd /srv/actions-runner && ./svc.sh stop && ./svc.sh uninstall'
sudo -u <RUNNER_USER> -H bash -lc 'cd /srv/actions-runner && read -rsp "Remove token: " REMOVE_TOKEN && printf "\n" && ./config.sh remove --token "$REMOVE_TOKEN" && unset REMOVE_TOKEN'remove token도 1시간 유효한 단기 값이며 외부에 출력하지 않습니다.sudo stat -Lc '%a %U:%G %n' <APPROVED_BACKUP_DIR>/.runner.<BACKUP_SUFFIX> <APPROVED_BACKUP_DIR>/.service.<BACKUP_SUFFIX> 2>/dev/null || true기존 설치에서만 선택합니다. identity backup은 증거이며 운영 경로로 복원하지 않습니다. fresh removal token으로 잔여 등록을 정리하고 새 registration token으로 재등록한 뒤 이전 enabled/active 상태를 승인 기록대로 복원합니다. B와 동시에 실행하지 않습니다.sudo systemctl disable --now 'actions.runner.<SCOPE>.<RUNNER_NAME>.service' || true
sudo install -d -o root -g root -m 0700 <APPROVED_QUARANTINE_DIR>
if ! sudo test -f <APPROVED_BACKUP_DIR>/.runner.<BACKUP_SUFFIX> && ! sudo test -f <APPROVED_BACKUP_DIR>/.service.<BACKUP_SUFFIX> && sudo test -d /srv/actions-runner; then sudo mv --no-clobber /srv/actions-runner <APPROVED_QUARANTINE_DIR>/actions-runner.new; fi변경 기록상 신규 설치이고 두 backup이 모두 없음을 확인할 때만 선택합니다. A와 동시에 실행하지 않으며 package·workspace·diagnostic log를 purge하지 않습니다.- GitHub UI에서 runner 등록이 제거됐습니다.
- service는 inactive·disabled입니다.
- 실패 artifact·work·diag는 0700 격리 경로에 보존됐습니다.
- remove token은 폐기됐습니다.
offline runner를 UI에서만 삭제하면 host service가 남을 수 있으므로 양쪽을 확인합니다.
원인·version·job 영향·격리 경로와 재등록 승인 조건을 기록합니다.
SECURITY CHECK
운영 전 마지막 보안 점검
- runner group·repository scope·labels를 최소화합니다.
- registration/remove token은 1시간 유효한 단기 값으로 대화형 입력하고 마스킹합니다.
- 비신뢰 PR은 지속형 self-hosted runner에서 실행하지 않습니다.
- 전용 비 root 계정이며 sudo·Docker socket·production key가 없습니다.
- ephemeral runner의 진단 로그는 외부 승인 저장소에 보존합니다.
- <...> 자리표시자는 승인된 리터럴 값으로 직접 치환하며 외부 입력으로 shell 명령을 조립하거나 eval하지 않습니다.
COMMON ERRORS
자주 막히는 지점
Runner가 Offline
- 증상
- GitHub UI에서 Offline이고 service는 실행 중입니다.
- 가능한 원인
- outbound DNS·TLS·proxy 또는 오래된 runner version 문제일 수 있습니다.
- 확인 순서
- config.sh --check와 제한 로그로 확인하고 TLS 검증을 끄지 않습니다.
job 뒤 상태가 남음
- 증상
- 후속 job이 이전 workspace·process를 봅니다.
- 가능한 원인
- 지속형 runner의 trust boundary가 충분하지 않습니다.
- 확인 순서
- 비신뢰 workload를 ephemeral 격리 환경으로 이전하고 host를 재이미징합니다.
등록 token 만료
- 증상
- config.sh가 token 오류로 등록에 실패합니다.
- 가능한 원인
- registration token 발급 후 1시간이 지났거나 scope가 다릅니다.
- 확인 순서
- 올바른 repository·organization 설정에서 새 token을 발급해 대화형으로 한 번만 입력합니다.
PRIMARY REFERENCES
공식 문서
- GitHub Docs · Adding self-hosted runners
- GitHub Docs · Configuring the runner application as a service
- GitHub Docs · Self-hosted runners reference
- GitHub Docs · Compromised runners
- actions/runner · 공식 릴리스
설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.