4. Agents

지난 글을 통해 GnuPG와 SSH를 모두 안전하게 설정했다면 이제 우리는 메세지를 암호화, 복호화, 서명하고 이를 증명할 수 있다. 그리고 비밀번호를 노출시킬 일도 없으며 brute-force 공격을 당할 가능성이 현실적으로 0에 가깝게 원격 서버에 인증할 수 있다. 다 좋은데, 여전히 이 연결 고리에서 한 가지 약점이 아직 남아있다 - 바로 우리의 passphrase이다.

자주 원격 서버에서 작업을 한다면, 대부분의 작업마다 passphrase를 입력하는 것은 꽤나 귀찮은 일이다. 그래서 passphrase 입력을 자동화한다거나, 아예 당신의 private key를 암호화되지 않은 상태로 두어서 passphrase를 쓰지 않고 싶은 생각이 들 수도 있다. 후자의 경우에, 보안에 많은 신경을 쓰는 우리는 분명히 private key가 절대 도난당하지 않도록 하고 싶을텐데, 여기서 SSH와 GnuPG를 위한 agents의 개념이 등장하게 된다.

Agent는 daemon인데, 이 daemon은 복호화된 private key들을 제한된 시간 동안 메모리에 안전하게 가지고 있게 하는 프로세스를 수행한다. 실제로 agent는 당신이 딱 한 번 passphrase를 입력해서 SSH나 GnuPG를 이용했다면, 그 복호화된 private key를 저장해서 다음 작업 때 사용하도록 도와준다.

이번 글에서는 SSH와 GnuPG를 위해 agent를 설정하는 기초적인 방법을 살펴볼 것이다. Agent가 어떻게 작동하는지 알고, 이를 이용해서 SSH와 GnuPG를 얼마나 편하게 사용할 수 있는지 알 수 있다.



SSH Agents

ssh-agent(1) 프로그램은 OpenSSH suite의 일부로 제공된다. 이 프로그램은 두 가지 모드로 실행될 수 있는데, 부모 프로세스로 실행되거나, 백그라운드에서 daemonized 되어서 실행될 수 있다. 여기서는 더 흔하게 쓰이고 더 유연하게 쓸 수 있는 후자의 방법을 설명할 것이다.


Setup

ssh-agent(1)를 처음으로 실행한다면, 이 실행 결과는 이해하기 어려울 수 있다. 아무 일도 하지 않고 그저 알아보기 어려운 shell 스크립트를 내뱉을 뿐인 것처럼 보이기 때문이다.

$ ssh-agent 
SSH_AUTH_SOCK=/tmp/ssh-EYqoH3qwfvbe/agent.28881; export SSH_AUTH_SOCK;
SSH_AGENT_PID=28882; export SSH_AGENT_PID;
echo Agent pid 28882;

하지만 여기서 알려주는 PID로 이미 daemon이 돌아가고 있음을 알 수 있다.

$ ps 28882
  PID TTY      STAT   TIME COMMAND
28882 ?        Ss     0:00 ssh-agent

그래서 daemon이 잘 돌아가고 있다면, 아까 출력된 shell 스크립트는 도대체 무엇인가? 그냥 알아서 실행시켜주면 안 되는 건가?

Unix process 모델의 구조를 잘 생각해보면 답이 나온다. Process는 그 부모의 환경을 수정할 수 없다는 특징 때문이다. 환경 변수 SSH_AUTH_SOCKSSH_AGENT_PIDssh(1)과 같은 프로그램들이 통신할 수 있는 agent를 찾을 수 있도록 고안된 것이기 때문에, 우리가 꼭 이 환경 변수를 설정해주어야 한다. 하지만 만약 ssh-agent(1)가 이 환경 변수를 직접 설정했다면, 이는 자기 프로세스 내에서만 적용이 될 것이고 우리가 사용하는 shell에 적용되지 않을 것이다.

그러므로 우리는 ssh-agent(1)을 실행할 뿐만 아니라, 그 결과로 출력되는 shell 스크립트도 꼭 실행시켜주어야 한다. 아래처럼 프로그램 실행 명령을 $(...)으로 감싸서 eval 하면 굉장히 편하게 할 수 있다.

$ eval "$(ssh-agent)"
Agent pid 3954

여기까지 실행하고 나면 우리는 ssh-agent(1)가 돌아가고 있을 뿐 아니라 이에 해당하는 socket path와 process ID를 가지고 있는 환경 변수가 생겼음을 알 수 있다.

$ pgrep ssh-agent
3954
$ env | grep ^SSH
SSH_AUTH_SOCK=/tmp/ssh-oF1sg154ygSt/agent.3953
SSH_AGENT_PID=3954

이제 우리를 위해서 key들을 관리해 줄 agent를 사용할 준비가 되었다.


사용법

다음 단계는 ssh-add(1) key를 agent에 넣는 것이다. 넣어두고 싶은 private key의 절대 경로를 넘겨주면 된다. 보통 private key는 ~/.ssh/id_rsa~/.ssh/id_dsa에 있다.

$ ssh-add ~/.ssh/id_rsa
Enter passphrase for /home/tom/.ssh/id_rsa:
Identity added: /home/tom/.ssh/id_rsa (/home/tom/.ssh/id_rsa)

~/.ssh에 있는 모든 기본적인 key type들 (id_dsa, id_rsa, id_ecdsa)을 모두 넣고 싶다면 파일 이름을 빼고 실행하면 된다.

$ ssh-add

어떻게 하든지 passphrase를 입력하라고 뜨게 될 것이다. 이는 정상적인 결과이므로, 안심하고 passphrase를 입력하자.

이제 방금 입력한 key를 확인하기 위해서 ssh-add(1)가 관리하고 있는 key를 리스팅해달라고 해보자.

$ ssh-add -l
4096 87:ec:57:8b:ea:24:56:0e:f1:54:2f:6b:ab:c0:e8:56 /home/tom/.ssh/id_rsa (RSA)

만약 다른 서버에 연결하기 위해 방금 넣어놓은 key를 사용한다면, 이제 우리는 더 이상 passphrase를 입력하지 않아도 된다. 곧장 로그인이 될 것이다.

tom@local:~$ ssh remote
Welcome to remote.sanctum.geek.nz, running GNU/Linux!
tom@remote:~$

ssh-add(1)는 기본 설정 상 key를 영원히 관리한다. Agent가 멈추거나 ssh-add -d <keyfile> 또는 ssh-add -D로 직접 지워질 때까지 말이다. 이 기본 설정에 대해 신경이 쓰인다면, ssh-add -t를 통해 시간 제한을 것 수 있다. 예를 들어 2시간 이후에 당신의 key를 잊어버리게 하려면 아래처럼 실행하면 된다.

$ ssh-add -t 7200 ~/.ssh/id_rsa

Agent를 완전히 죽이고 싶다면 ssh-agent -k를 사용하면 되는데, 마찬가지로 eval $(...) 구문을 사용하는 것이 편하다.

$ eval "$(ssh-agent -k)"
Agent pid 4501 killed

돌고 있는 agent가 속해있는 당신의 session이 끝날 때 agent도 멈추게 하고 싶다면, ~/.bash_logout과 같은 스크립트에 넣어둘 수도 있을 것이다.


영구적인 Setup

사용해보니 맘에 든다면 ~/.bash_profile과 같은 startup 스크립트에 넣어두어도 좋다. 이렇게 하면 agent는 login shell이 시작할 때마다 같이 시작될 것이고, 이로부터 실행되는 subshell (xterm, screen, 잘 설정된 tmux 등) 에서도 같은 효과를 누릴 수 있다.

eval "$(ssh-agent)"
ssh-add ~/.ssh/id_rsa

다음에 TTY 로그인 할 때면 passphrase를 입력하라고 바로 뜨게 될 것이고, 이후에는 agent가 관리하고 있는 key를 이용해서 가능한 모든 서버에 곧장 연결이 가능하게 된다.

tom@local:~$ ssh remote
Welcome to remote.sanctum.geek.nz, running GNU/Linux!

GDM이나 XDM 같은 desktop manager 환경에서도 위와 같은 설정을 하고 싶다면, ssh-askpass(1) 프로그램을 가리키는 환경 변수를 추가하면 된다.

eval $(ssh-agent)
export SSH_ASKPASS=/usr/bin/ssh-askpass
ssh-add ~/.ssh/id_rsa

SSH_ASKPASS가 위처럼 정의되고 DISPLAY가 현재 작업 중인 display를 가리킨다면, 이제 passphrase를 입력받기 위해서 아래와 같은 간단한 graphical prompt가 나타날 것이다.

A simple graphical prompt for ssh-add via ssh-askpass

이 프로그램은 따로 설치해야 할 수도 있다. Debian 계열의 시스템에서의 package 이름은 ssh-askpass 이다.

Login shell의 agent과 관련된 환경 변수들을 export를 통해 저장했으므로, 모든 자식 process들과 subshell들은 이 환경 변수들을 모두 넘겨받아간다.

tom@local:~$ screen
tom@local:~$ tmux bash
tom@local:~$ bash
tom@local:~$ ssh remote
Welcome to remote.sanctum.geek.nz, running GNU/Linux!
tom@remote:~$

즉, login session에서 딱 한 번 passphrase를 입력한다면 그 후에는 서버들에 접속하기 위해서 어떤 key를 사용하는지 선택하고 그에 해당하는 passphrase를 입력하지 않아도 되는 것이다... 무척 편리하다!



GPG Agents

ssh-agent(1)와 마찬가지로, GnuPG key를 관리하는 agent 역시 존재한다. 그 이름은 gpg-agent(1)이다. 이 프로그램이 하는 일 역시 매우 비슷하다. Debian 계열의 시스템에서는 gnupg-agent package 로 설치할 수 있다. 또한 pinentry 프로그램을 설치하길 바란다. 여기서는 명령줄에서의 구성 요소를 배우는 데 집중할 것이므로, pinentry-curses(1) 을 사용해 콘솔 기반에서 passphrase를 입력받을 것이다.

# apt-get install gnupg-agent pinentry-curses


Setup

ssh-agent에서 배웠던 eval $(...) 트릭을 똑같이 사용할 것이다.

$ eval "$(gpg-agent --daemon)"

여기서 알려주는 PID를 통해서 agent가 돌고 있는지 확인할 수 있고, 이에 더해서 새로운 환경 변수가 추가된 것도 볼 수 있다.

$ pgrep gpg-agent
5131
$ env | grep ^GPG
GPG_AGENT_INFO=/tmp/gpg-hbro8r/S.gpg-agent:5131:1

그리고 pinentry 프로그램이 어떤 terminal에서 passphrase를 받는 화면을 그려야 할 지 알 수 있도록 도와주는 GPG_TTY 환경 변수도 설정해주어야 한다.

$ export GPG_TTY=$(tty)
$ echo $GPG_TTY
/dev/pts/2

마지막으로 gpg(1)가 이 agent를 정말로 사용하도록, ~/.gnupg/gpg.conf 에 다음 한 줄을 추가한다. 파일이 없다면 만들어서 추가하자.

use-agent


사용법

설정을 모두 마쳤다면 이제 private key를 필요로 하는 모든 작업에 대해서 그 passphrase 입력이 명령줄에서 직접적으로 이루어지지 않고, PIN entry 프로그램을 통해서 이루어질 것이다.

$ gpg --armor --sign message1.txt

┌──────────────────────────────────────────────────────────┐
│ You need a passphrase to unlock the secret key for user: │
│ "Thomas Ryder (tyrmored, tejr) <tom@sanctum.geek.nz>"    │
│ 4096-bit RSA key, ID 25926609, created 2013-03-12        │
│ (main key ID 77BB8872)                                   │
│                                                          │
│                                                          │
│ Passphrase ***__________________________________________ │
│                                                          │
│       <OK>                                 <Cancel>      │
└──────────────────────────────────────────────────────────┘

Passphrase를 입력하면, 본래 작업이 진행된다.

$ ls message1*
message1.txt
message1.txt.asc

그 다음으로 private key를 필요로 하는 다른 작업을 할 때, 더 이상 passphrase를 물어보지 않는다.

$ gpg --armor --sign message2.txt
$ ls message2*
message2.txt
message2.txt.asc

즉 agent가 private key를 cache해서 연속된 작업들을 훨씬 간편하게 수행하도록 해주는 것이다. 기본 설정 값으로 시간 제한은 10분인데, 이는 ~/.gnupg/gpg-agent.confdefault-cache-ttlmax-cache-ttl 설정을 통해 바꿀 수 있다. 예를 들어 private key를 마지막으로 사용한 이후 한 시간, 처음으로 사용한 이후 두 시간 동안 저장하고 있도록 하고 싶다면, 아래처럼 설정하면 된다.

default-cache-ttl 3600
max-cache-ttl 7200

이렇게 변경한 설정을 적용시키려면 agent를 reload시켜야 한다.

$ gpg-connect-agent <<<RELOADAGENT
OK


영구적인 Setup

ssh-agent(1)와 마찬가지로, an ideal place for gpg-agent(1)의 startup 명령이 있을 바람직한 곳은 ~/.bash_profile와 같이 login shell setup 스크립트이다.

eval "$(gpg-agent --daemon)"

Login shell을 시작할 때 agent가 함께 시작되고, 관련된 환경 변수들이 정의되며, 그 변수들은 이후의 모든 subshell들이 넘겨받는다. ssh-agent에서 봤듯이 말이다.

만약 콘솔 PIN entry 도구를 사용한다면 아래 한 줄을 interactive shell의 startup 스크립트에 추가해주어야 한다. 이 명령은 Linux Bash에서는 ~/.bashrc에 있으면 된다. Mac OS X에서도 마찬가지일 수도 있다.

export GPG_TTY=$(tty)



Keychain

ssh-agent(1)gpg-agent(1) 두 프로그램을 효과적으로 관리하기 위한 keychain(1)이라는 프로그램이 있다. 이 프로그램은 두 agent를 하나의 명령으로 시작하는 간편한 방법을 제공하는데, startup 시에 여러 key들을 가져오고, 시스템 어느 곳에서 이미 시작한 agent를 가져옴으로써 각 agent가 두 번 실행되는 것을 방지한다. 데스크탑 환경에서는 여러 사용자를 위해 두 agent 중 하나 또는 두 개 모두를 시작하도록 종종 설정이 되어 있기 때문에, 이미 돌아가고 있는 agent를 가져와서 사용하는 것은 효율적인 일이다. 이런 점에서 keychain(1)이 강력한 기능을 하는 것이다.

On Debian-derived systems, the program is available in the keychain package: Debian 계열의 시스템에서는, keychain package 에서 설치할 수 있다.

# apt-get install keychain

keychain이 설치되었다면, ~/.bash_profile에 아래의 단 하나의 명령을 추가해서 두 agent를 모두 시작할 수 있다.

eval "$(keychain --eval)"

We can optionally include the filenames of SSH keys in ~/.ssh or the hex IDs of GnuPG keys as arguments to prompt loading the private key (including requesting the passphrase) at startup: 선택적으로 ~/.ssh에 있는 SSH key의 파일 이름이나 GnuPG의 여러 hex ID를 argument로 넣어서 startup 시의 해당 private key를 불러오도록 (이를 위해 첫 passphrase를 요청하도록) 명령할 수도 있다.

eval "$(keychain --eval id_rsa 0x77BB8872)"

이 프로그램을 사용할 수 있는 환경이라면 필자는 꼭 사용하기를 추천한다. Agent들과 그 환경을 관리하는 것은 꽤 성가신 일이다. keychain(1)는 이런 잡일을 모두 해주기 때문에 특정 상황에 맞는 agent가 잘 작동하고 있는지에 대해서 걱정할 필요가 없어진다. 이 프로그램에 대해 더 많은 정보를 보려면 프로젝트 홈페이지 를 방문해보자.