개요

사용자가 엘릭서 코드를 입력하면 실행한 뒤 그 결과값을 반환하는 엘릭서 어플리케이션을 만들고 있는데, 이런 앱에서는 보안이 정말 중요합니다. 인터넷 어딘가에 존재하는 해커가 작성한 악질적인 코드를 실행하게될 수도 있으니까요. 이런 것을 만들어보는 것은 처음이라서 좋은 해법이 있나 찾아봤습니다.

안타깝게도 항상 그러하듯이 완벽한 해법은 없었습니다. 득실을 저울질해본 결과 보안 설정을 해놓은 도커 컨테이너를 사용하기로 결정했는데, 제 경우에는 적절한 보안과 성능을 제공하는 안이라고 생각합니다. 이 글에서는 이 앱을 만드는 과정에서 어떤 것을 배웠나 기록하고자 합니다.

이 주제에 관해서는 저도 초보기 때문에 이 글은 기술적 분석이 아니라 개인적인 경험담으로 읽어주셨으면 좋겠습니다. 혹시 틀린 내용이 있으면 지적해주시고, 추가적으로 볼만한 자료가 있으면 알려주시면 감사드리겠습니다. 참고로 샌드박싱 기술 전반 현황을 보시고 싶으시면 Marek(@majek04)가 작성한 Sandboxing landscape를 추천드리고 싶습니다. 제가 판단할 수 있는 수준 하에서는 최신 정보를 폭 넓게 망라한 글입니다.

무엇을 만들고자 하는가

엘릭서를 배울 수 있는 웹사이트를 만들려고 하는데, 설명하는 내용 중간에 연습문제를 바로 풀 수 있는 형식으로 만들려고 합니다. 비슷한 튜토리얼 웹사이트도 이미 많고, 브라우저에서 실행되는 REPL도 이미 많으니 기술적으로는 매우 간단할 것이라고 생각했습니다. 하지만 그렇게 쉽게 풀릴리가 없죠. 무엇인가 기술적으로 간단하다고 말하는 사람은 그게 설령 과거의 나라도 절대 믿으면 안된다는 교훈을 다시금 되새겼습니다.

여기서 기술적으로 어려운 부분은 어떤 코드든 안전하게 컴파일하고 실행할 수 있는 샌드박스 환경을 제공하는 것입니다. 간단한 포크 폭탄 때문에 서버가 터지거나, 크래커한테 서버가 탈취당하면 곤란하니까요. 여러 가지 방안을 생각해보았지만 각 방법마다 문제점이 있다는 것을 알게 되었습니다.

코드 수준 보안책

처음 고려한 방법은 위험요소가 있는 코드를 블랙리스트하는 것이었습니다. 얼랭 VM이나 운영체제에 영향을 미칠 수 있는 함수 등이 여기에 해당되죠. 사용자가 입력한 코드의 추상 구문 트리(abstract syntax tree, AST)를 분석해서 블랙리스트 해놓은 함수를 감지하는 식으로 구현할 수는 있을텐데, 조금만 생각해보니 제대로 구현하기 불가능한 방법이라는 것을 깨달았습니다. 공격자가 언제든 새로운 우회방식을 만들어낼 수 있는 상황에서 함수를 정의하고 불러오고 실행할 수 있는 모든 가능성을 블랙리스트하는 것은 현실적으로 불가능합니다.

그 다음으로 고려한 방법은 안전한 코드를 화이트리스트하는 것이었습니다. 하지만 이는 사용자가 프로그래밍을 배우면서 어떤 엘릭서 코드든 실행해볼 수 있도록 해주겠다는 제 목표에 부합하지 않는 방식이었습니다. 그뿐 아니라 엘릭서 언어 소스 코드 자체를 살펴보니, 악용될 가능성이 있는 함수에 의존하는 함수도 많이 있었습니다. 결국 이 방법을 택하려면 엘릭서 언어를 포크해서 제가 원하는 버전을 새로 만들어야 한다는 것을 깨달았습니다. 전 그런 일을 할 능력도 없고, 하고 싶지도 않았습니다.

OS 프로세스 수준 보안책

OS 프로세스 수준에서 샌드박싱을 하는 것이 결국 더 현실적인 접근법이라 생각했습니다. 처음 생각한 방식은 도커 컨테이너를 샌드박스로 쓰는 것이었는데, 다른 사람들이 도커를 그런 용도로 쓰는 것을 이미 봤기 때문에 문제가 없는 방식일 것이라고 생각했었죠. 하지만 조금 더 알아보니 도커가 그렇게 안전한 방식이 아니란 것을 알게 되었습니다. 도커의 개발 목표는 샌드박싱 문제 해결이 아니며, 어느 정도 보안을 제공하긴 하지만 상황에 따라서 충분히 뚫릴 수 있다는 말이었습니다.

보안 공격이 어떻게 일어날 수 있는지에 대해서 논의를 좀 읽어봤지만 사실 제대로 이해하지는 못 했습니다. 하지만 실용성이 있으면서도 완벽한 보안을 제공하는 샌드박스는 존재할 수 없다는 것은 확실히 알게되었습니다. 완벽하게 격리된 샌드박스는 완벽한 보안을 제공하지만, 그러면 샌드박스를 사용할 수 있는 방법도 없기 때문에 아무 쓸모가 없습니다.

샌드박스의 원리 상 샌드박스가 호스트 시스템에서 더 철저히 격리될수록 더 안전해집니다. 이는 호스트 시스템과 커널을 공유하는 도커 컨테이너보다는 호스트 시스템의 커널 위에서 별도의 커널을 실행하는 VM이 더 안전할 가능성이 높다는 것을 의미합니다. 그러면 VM을 쓰는 것이 옳을까요?

문제는 온전한 VM은 도커보다 훨씬 더 많은 자원을 필요로 한다는 것입니다. 도커의 가장 큰 매력 중 하나가 기존 VM에 비해서 자원을 상대적으로 더 효율적으로 사용할 수 있다는 것이니까요. 달리 말해 도커를 사용하면 동일한 하드웨어 자원을 사용해서 더 많은 사용자를 지원할 수 있습니다. 보안과 사용량 사이에서 저울질을 해야하는 상황입니다.

하지만 제 앱에서는 사용자가 입력한 코드를 실행할 때마다 매번 새로운 샌드박스 환경을 생성해야 한다는 것을 인식하고 나니, 자원을 조금이라도 더 효율적으로 사용하는 것이 더 중요하다는 결론을 내렸습니다. 앞서 말한 대로 사용자가 실행할 수 있는 코드에 제한을 두지 않기로 했으니, 샌드박스 환경이나 그 위에서 돌아가는 얼랭 VM에 영향을 미칠 수 있는 코드가 실행될 수도 있다는 가능성을 고려해야 했습니다. 그런 경우 각 코드가 독립적으로 컴파일되고 실행된다는 보장을 할 수 있는 유일한 방법은 매번 새로운 샌드박스를 제공하는 것뿐이었습니다. 물론 이는 자원을 매우 많이 소비하는 방식입니다. 최초로 도커를 사용해서 이를 구현해보았을 때도 앱이 정말 느려지는 것을 볼 수 있었습니다. 도커를 사용하는 것도 이렇게 느리다면 VM을 사용하면 앱을 사용할 수 없을 정도로 느려질 것이 분명했습니다. 그래서 일단은 도커를 사용하기로 결정했습니다.

단상

샌드박싱이 기술적으로 이렇게 복잡한 문제일 것이라고는 전혀 생각하지 못했습니다. 하지만 이 사안에 대해서 여러가지 글을 읽고 생각해본 결과, 어떤 코드를 실행하건 무관하게 보안을 보장하는 것은 이론적으로 불가능한 것이 아닐까 생각합니다. 정확히 증명할 수는 없지만 문제의 형태를 보면 컴퓨터 과학에서 결정불가능한 문제 중 하나가 아닐까 추측합니다. 이런 경우 보안을 보장하기 위해서는 프로그램의 능력을 어떤 식으로든 제한해야 하는데, 저는 도커 컨테이너를 샌드박스로 사용해서 샌드박스 내 프로그램의 능력은 제한하지 않는 대신 샌드박스 외부에 영향을 미칠 수 있는 능력은 제한하기로 했습니다.

적절히 설정된 도커 컨테이너를 사용하면 합리적인 수준의 보안과 성능을 확보할 수 있으므로 제가 필요로 하는 조건은 충족됩니다. 제가 만드는 것은 보안이 매우 중요한 앱이 아니니 이 정도 취약 가능성은 수용할 수 있으니까요. 하지만 보안이 치명적인 시스템에서 샌드박스를 구현해야한다면 다른 방안을 선택해야 하리라 생각합니다. 이번 앱을 만들면서 그 정도 안목은 키울 수 있었습니다.

SmartOS Zones: 해결책?

솔라리스에 기반한 운영체제인 SmartOS와 거기서 제공하는 Zones 기술을 사용하면 안전한 샌드박스를 구현할 수 있다는 글을 보았는데, 해당 OS 자체가 가상화와 보안을 최우선적으로 염두에 두고 설계되었기 때문이라고 합니다. 좀 더 보안이 확실한 샌드박스를 만들어야 한다면 이를 고려해봐야겠습니다.