[번역] OpenSSH의 기본 키 암호화는 평문보다 못합니다

[번역] OpenSSH의 기본 키 암호화는 평문보다 못합니다

이 글은 Latacora에서 작성한 The default OpenSSH key encryption is worse than plaintext의 번역글입니다. 암호화에 대한 지식이 깊지 않고 영문 번역 전문가가 아니기 때문에 오역이 많을 수 있습니다. (많을 거라 확신합니다.) 이에 대해서는 개인 이메일로 문의주시면 바로 수정하겠습니다. 감사합니다.

최근에 eslint-scope npm 패키지가 해커의 손에 넘어가 npm 사용자의 홈 디렉토리로부터 npm 인증 정보를 훔쳐갔던 일이 있었습니다. 우리는 차례대로 짚어보면서 어떤 것들이 문제였고 어떻게 위험을 줄일 수 있는지 알아봤습니다.

많은 사람들이 RSA SSH 키를 갖고 있습니다. 이 SSH키는 대부분의 권한을 갖고 있습니다: production 및 GitHub에 액세스 할 권한 같은 것들 말이죠. npm 인증 정보와는 반대로 SSH 키는 암호화됩니다, 자 그럼 이게 유출되어도 안전할까요? 한번 짚어보죠!

user@work /tmp $ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/user/.ssh/id_rsa): mykey
...
user@work /tmp $ head -n 5 mykey  
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,CB973D5520E952B8D5A6B86716C6223F

+5ZVNE65kl8kwZ808e4+Y7Pr8IFstgoArpZJ/bkOs7rB9eAfYrx2CLBqLATk1RT/

뭐 암호화가 된 것처럼 보이니까 암호화 됐다고 말할 수도 있겠습니다. 잘 보니까 MII(RSA키임을 알 수 있는 base64 DER 단서)로 시작하지는 않네요. 네 AES입니다! 참 좋아요, 그쵸? 심지어, 표면상으로는 랜덤으로 생성된 IV[1]를 CBC[2]에 쓰고 있어요. MAC은 없지만, oracle padding같은 것들은 없으니까 괜찮겠죠?

이 DEK-Info가 의미하는 것이 무엇인지 찾아내는 것은 복잡합니다. openssh-portable 레포에서 이 DEK-Info 문자열을 찾아보면 샘플 키만 나옵니다. 핵심은 이 AES키는 그저 MD5(비밀번호 || IV[:8])이라는 점입니다. 정말 좋지 않아보이죠: 가장 좋은 비밀번호 저장소의 사례는 낮은 엔트로피로 비밀번호를 보관하고, 암호화에 필요한 자료로 변형하기 위해 Argon2와 같은 암호화하는데 매우 큰 비용이 드는 기능을 쓰는 것입니다. MD5는 암호화 비용이 매우 적습니다. 이 디자인이 갖는 유일한 것은 salt가 암호 뒤에 붙는 것 뿐입니다, 따라서 MD5(IV[:8])의 중간 상태를 연산하고 해당 값으로부터 비밀번호를 알아낼 수는 없습니다. 특별히 초당 수십억번의 MD5 연산을 할 수 있는 머신을 빌릴 수 있는 요즘 세상에선 칭찬받을 부분은 아닙니다. 뭐 그리 많은 비밀번호가 나오지는 않지만요.

아마도 OpenSSH가 어떻게 이따위로 구성되게 되었는지 물어볼 수 있겠습니다. 슬픈 답은 OpenSSL 커맨드 라인 툴이 이를 기본값으로 지정하고 있고, 이 때문에 이렇게 되어있는 것이죠.

표준화된 비밀번호 암호화 키가 평문보다 나을 수 없다고 하는 것에는 논쟁의 여지가 있습니다: 이 암호화는 아무 효과가 없습니다. 하지만 저는 강한 성명을 내놓겠습니다: 이건 평문보다 훨씬 나쁩니다. 이 주장은 간단합니다: SSH키 비밀번호가 비밀번호 관리자로 다뤄지는 경우는 거의 적습니다: 대신 그저 당신이 기억하는 것일 뿐이죠. 이걸 당신이 기억하고 있다면, 이걸 다른 어딘가에 또 사용하고 있을 수도 있겠죠. 당신이 쓰고 있는 기기의 비밀번호일 수도 있고요. 이 유출된 키는 오라클(oracle)을 제공합니다: 만약 제가 비밀번호를 정확하게 추측한다면(KDF[3]가 나쁘기 때문에 가능한 일입니다) 저는 제가 정확하게 추측했음을 알 수 있습니다, 왜냐하면 당신의 공개 키를 이용해 이를 검사할 수 있기 때문이죠.

RSA키 쌍 자체에는 아무런 문제가 없습니다: 이건 그저 개인키(private key)의 대칭 암호화일 뿐입니다. 공개키만 있다고 해서 공격에 사용할 순 없습니다.

어떻게 고칠 수 있을까요? OpenSSH에서 꼭 사용해야 할 "새(New)" 키 포맷이 있습니다. "New"가 의미하는 것은 2013입니다. 이 포맷은 bcrypt_pbkdf를 사용하는데 이는 고정적인 난이도(difficulty)를 가진 bcrypt에, pbkdf2 구성으로 작동하는 포맷입니다. 다행히도, 만약 당신이 Ed25519 키를 생성하려 할 때 이 new 포맷이 생성됩니다, 왜냐하면 오래된 SSH 키 포맷은 더이상 새로운 키 타입을 지원하지 않기 때문입니다. 이건 좀 이상한 주장입니다: Ed25519 그 자체가 이미 어떻게 직렬화(serialization)를 할지 정의하고 있기 때문에 어떻게 Ed25519 직렬화가 작동할지에 대해 정의하는데 키 포맷이 필요하지 않습니다. 하지만 만약 이게 어떻게 우리가 좋은 KDFs를 얻을 수 있는지라면, 이건 제가 다루고자 했던 부분이 아닙니다. 따라서 한가지 답은 ssh-keygen -t ed25519입니다. 만약 호환성의 이유로 RSA를 꼭 사용해야만 한다면 ssh-keygen -o를 사용할 수 있습니다. 이미 존재하는 키는 ssh-keygen -p -o -f PRIVATEKEY로 업그레이드할 수 있습니다. 만약 키가 Yubikey나 스마트 카드에 존재한다면 이 문제가 발생하지 않습니다.

저는 이에 대해 더 나은 답을 드리고 싶습니다. 다른 한편으로 aws-vault는 인증 정보를 디스크에서 키체인으로 옮기는 방법을 보여줬습니다. 다른 병렬 접근으로는 개발 환경을 분할된 환경으로 옮기는 것입니다. 마지막으로, 대부분의 스타트업은 오랫동안 SSH 키를 보유하지 않는 것이 좋습니다. 대신 SSH CA에 의해 발행된 임시 인증 정보를 사용하여 SSO를 이상적으로 두는 것이 좋습니다. 불행하게도 이는 GitHub에서 작동하지 않습니다.

PS: 신뢰할 수 있는 소스는 찾는 것은 어렵지만, 제 기억에 따르면: PEM과 같은 OpenSSH 개인키에 포함된 버전 파라미터는 암호화 방식에만 영향을 끼칩니다. 애초에 망가진 KDF기 때문에 이는 그리 중요하지 않습니다. 이는 프로토콜의 단편적인 협상 부분에 대한 논쟁이라고 저는 확신합니다, 이에 대해서는 나중에 블로그에 올려보겠습니다. 어느날 john the ripper를 돌려보고 싶다면, 이 키를 써보세요: https://gist.github.com/lvh/c532c8fd46115d2857f40a433a2416fd


  1. Initial Vector; 초기 block의 유추를 어렵게 key와 함께 포함 ↩︎

  2. Cipher Block Chaining; 이전 block을 다음 block의 입력으로 사용하여 안정성 향상 ↩︎

  3. Key Derivation Function; 키 생성 방식 ↩︎