Ver2.5 서버리스 시대, 우리 앱은 안녕하신가요? 힙한 개발자를 위한 서버리스 보안 완전정복 가이드 🚀

서버리스 시대, 우리 앱은 안녕하신가요? 힙한 개발자를 위한 서버리스 보안 완전정복 가이드 🚀
서버가 없어져서 편하다는 건 옛말! 이제는 보이지 않는 위협과 싸워야 할 때.
클라우드 네이티브의 심장, 서버리스 보안의 모든 것을 친구처럼 알려줄게!
👋 프롤로그: 서버리스, 구름 위를 걷는 기분? 근데 발밑은 괜찮아?
안녕, 개발자 친구들! 요즘 '서버리스(Serverless)' 모르면 간첩 소리 듣는 시대잖아. 코딩만 하면 알아서 척척 돌아가고, 쓴 만큼만 돈 내고, 트래픽이 몰려도 걱정 없는 마법 같은 기술! ✨ 마치 개발자를 위한 유토피아 같지. 인프라 관리의 족쇄에서 벗어나 오롯이 비즈니스 로직에만 집중할 수 있다니, 이건 정말 혁명이야.
AWS Lambda, Azure Functions, Google Cloud Functions... 이름만 들어도 가슴이 웅장해지는 이 서비스들 덕분에 우리는 더 빠르고 유연하게 제품을 만들 수 있게 됐어. "서버가 없다니! 이제 서버 관리자도, 보안 걱정도 없는 건가?" 라고 생각했다면... 땡! 땡! 땡! 🚨 아주 큰 착각이야.
오히려 그 반대야. 서버가 '없어진' 게 아니라, '추상화'된 거거든. 우리 눈에 보이지 않을 뿐, 어딘가에선 수많은 컴퓨터가 우리의 코드를 실행하고 있어. 그리고 해커들은 바로 그 보이지 않는 틈을 노리고 있지. 예전에는 성문만 잘 지키면 됐는데, 이제는 성 안에 흩어져 있는 수백, 수천 개의 작은 초소(함수)들을 전부 지켜야 하는 상황이 된 거야. 공격 표면(Attack Surface)이 어마어마하게 넓어졌다는 뜻이지.
클라우드 서비스 제공자(AWS, Google 등)와 우리(사용자)가 보안 책임을 나눠 갖는다는 개념이야. 클라우드 제공자는 '클라우드 자체의 보안(Of the Cloud)'을 책임져. 예를 들어 데이터 센터 물리 보안, 하드웨어, 네트워킹 같은 것들 말이야. 하지만 우리는 '클라우드 안에서의 보안(In the Cloud)'을 책임져야 해. 우리가 올린 코드, 데이터, 접근 권한 설정, 네트워크 구성 같은 것들! 서버리스라고 해서 이 책임이 사라지는 건 절대 아니야. 오히려 더 중요해졌지.
이 글에서는 서버리스 환경이 기존의 웹 애플리케이션 환경과 보안적으로 뭐가 다른지, 어떤 새로운 위협들이 도사리고 있는지, 그리고 이 위협들로부터 우리의 소중한 앱과 데이터를 어떻게 지켜낼 수 있는지 아주 탈탈 털어볼 거야. 이론만 늘어놓는 지루한 강의가 아니라, 당장 내 프로젝트에 적용할 수 있는 실전 꿀팁과 체크리스트까지 담았으니 끝까지 함께 달리자고! 🏃♀️🏃♂️
Chapter 1. 서버리스 보안, 도대체 뭐가 다른데?
"보안이 거기서 거기지, 뭐 특별한 게 있겠어?" 라고 생각할 수도 있어. 맞아, SQL 인젝션이나 XSS 같은 고전적인 공격들은 여전히 유효해. 하지만 서버리스는 아키텍처 자체가 완전히 다르기 때문에, 공격의 벡터와 방어의 포인트가 확 바뀌었어. 어디가 어떻게 다른지 비교하면서 알아보자.
1. 공격 표면: '점'에서 '면'으로, 아니 '우주'로! 🌌
전통적인 모놀리식(Monolithic) 아키텍처를 생각해 봐. 보통 웹 서버, 애플리케이션 서버, 데이터베이스 서버 몇 대로 구성되지. 해커가 노릴 곳은 비교적 명확했어. 웹 서버의 특정 포트, 애플리케이션의 로그인 페이지 같은 '점'들이 주된 타겟이었지. 방화벽으로 성벽을 단단히 쌓고, 그 안만 잘 지키면 어느 정도 안심할 수 있었어.
하지만 서버리스는? 수십, 수백 개의 작은 함수(Function)들이 각각 API Gateway 엔드포인트, 클라우드 스토리지 이벤트, 데이터베이스 트리거 등 다양한 이벤트 소스에 연결되어 있어. 이 모든 것 하나하나가 잠재적인 침입 경로가 될 수 있는 거야. 공격 표면이 '점'들의 집합이 아니라, 거미줄처럼 얽히고설킨 '네트워크' 그 자체가 된 거지.
2. 사라진 경계, 이제는 '제로 트러스트' 시대 🛡️
전통적인 보안은 '성벽 모델' 또는 '경계 보안(Perimeter Security)' 모델에 기반했어. 일단 성벽(방화벽) 안으로 들어오면, 내부의 시스템들은 서로를 어느 정도 신뢰하는 경향이 있었지. "일단 우리 편이니까 괜찮겠지" 하는 생각이야.
하지만 서버리스에서는 이 '경계'라는 개념 자체가 모호해져. 모든 함수와 서비스가 인터넷을 통해 통신할 수 있고, 외부 서비스와도 자유롭게 연동되거든. 그래서 '제로 트러스트(Zero Trust)'라는 개념이 필수적이 되었어. "아무도 믿지 마라. 항상 검증하라(Never Trust, Always Verify)." 이게 핵심이야. 내부 통신이든 외부 통신이든, 모든 요청은 그 정체성과 권한을 철저하게 확인해야 해.
3. 짧은 생명주기: 양날의 검 ⚔️
서버리스 함수는 보통 요청이 있을 때만 잠깐 살아났다가, 일이 끝나면 사라져. 이런 '일시적인(Ephemeral)' 특성은 보안에 장점이 되기도 해. 해커가 시스템에 침투해도, 함수가 곧 사라져 버리니 지속적인 거점을 마련하기가 어렵거든. 악성 코드를 심어놔도 다음 실행 땐 깨끗한 환경에서 다시 시작되니까.
하지만 이건 단점이기도 해. 문제가 발생했을 때 로그를 남기거나 상태를 분석하기가 까다로워. 범인이 잠깐 나타났다가 연기처럼 사라져 버리는 셈이지. 그래서 강력한 로깅과 모니터링 시스템을 미리 구축해두지 않으면, "뭔가 이상한데... 뭐가 문제인지 모르겠네?" 하는 상황에 빠지기 쉬워.
4. 과도한 권한: 슈퍼맨 망토를 입은 신입사원 🦸
서버리스 개발을 하다 보면, 귀찮다는 이유로 함수에 과도한 권한을 부여하는 경우가 정말 많아. 예를 들어, 데이터베이스의 특정 테이블에서 읽기만 하면 되는 함수에게 읽기, 쓰기, 삭제 권한을 전부 다 주는 거지. AWS에서는 `AdministratorAccess` 같은 '만능키'를 덥석 줘버리기도 해.
이건 마치 신입사원에게 회사 기밀문서 접근 권한, 법인카드, CEO 사무실 열쇠까지 전부 줘버리는 것과 같아. 만약 이 함수 하나가 해커에게 털리면? 해커는 그 함수가 가진 모든 권한을 이용해 데이터베이스를 날려버리거나, 다른 시스템으로 공격을 확장하는 등 끔찍한 일을 저지를 수 있어. '최소 권한의 원칙(Principle of Least Privilege, PoLP)'은 서버리스 보안의 알파이자 오메가야.
5. 서드파티 의존성: 믿는 도끼에 발등 찍힌다 🪓
서버리스 애플리케이션은 레고 블록처럼 여러 서비스를 조립해서 만드는 경우가 많아. 인증은 Auth0, 데이터베이스는 MongoDB Atlas, 검색은 Algolia... 이런 식으로 말이야. 그리고 함수 코드 자체도 수많은 오픈소스 라이브러리(npm 패키지 등)에 의존하지.
이건 개발 속도를 엄청나게 높여주지만, 동시에 '공급망 공격(Supply Chain Attack)'의 위험을 키워. 내가 직접 짠 코드는 안전해도, 내가 가져다 쓴 라이브러리에 악성 코드가 숨어있다면? 혹은 내가 연동한 서드파티 서비스가 해킹당한다면? 그 피해는 고스란히 우리 서비스로 이어질 수 있어. 내가 사용하는 모든 '부품'들의 보안 상태를 주기적으로 점검해야 하는 숙제가 생긴 거지.
Chapter 2. OWASP Top 10, 서버리스 버전으로 재해석하기 🧐
개발자라면 'OWASP Top 10'을 한 번쯤은 들어봤을 거야. 웹 애플리케이션 보안에서 가장 중요하고 흔하게 발견되는 10가지 취약점을 정리한 목록이지. 그런데 서버리스 환경은 기존 웹 환경과 다르다고 했잖아? 그래서 OWASP에서도 '서버리스 Top 10'을 따로 발표했어. 이걸 하나씩 뜯어보면서 우리의 앱을 어떻게 지킬 수 있을지 알아보자.
S1: Injection (인젝션)
S2: Broken Authentication (인증 실패)
S3: Sensitive Data Exposure (민감 데이터 노출)
S4: XML External Entities (XXE) (XML 외부 개체)
S5: Broken Access Control (접근 제어 실패)
S6: Security Misconfiguration (보안 설정 오류)
S7: Cross-Site Scripting (XSS) (크로스 사이트 스크립팅)
S8: Insecure Deserialization (안전하지 않은 역직렬화)
S9: Using Components with Known Vulnerabilities (알려진 취약점이 있는 구성 요소 사용)
S10: Insufficient Logging & Monitoring (불충분한 로깅 및 모니터링)
S1: 인젝션 (Injection) - "네 말을 곧이곧대로 믿은 내 잘못이지..."
인젝션은 예나 지금이나 부동의 1위 취약점이야. 사용자가 입력한 데이터를 검증 없이 코드나 쿼리의 일부로 사용했을 때 발생해. 서버리스에서는 그 형태가 더 다양해졌어.
* SQL/NoSQL 인젝션: 함수가 데이터베이스와 통신할 때 발생. 사용자가 입력한 값을 그대로 쿼리 문자열에 합쳐서 실행하면 큰일 나! * OS 명령어 인젝션: 함수 내에서 `exec()` 같은 함수로 외부 셸 명령어를 실행할 때, 사용자 입력값을 필터링 없이 사용하면 시스템 명령어를 해커 마음대로 실행할 수 있게 돼. * 이벤트 데이터 인젝션: 서버리스 함수는 HTTP 요청뿐만 아니라 S3 파일 업로드, DynamoDB 스트림 등 다양한 이벤트에 의해 트리거될 수 있어. 이 이벤트 데이터에 악성 페이로드가 포함될 수 있다는 사실을 항상 명심해야 해. 예를 들어, 파일 이름에 `../../etc/passwd` 같은 경로 조작 문자를 넣어 시스템 파일에 접근을 시도할 수도 있지.
🛡️ 방어 전략:
1. 입력값 검증 (Input Validation): 모든 외부 입력(HTTP 요청 본문, 쿼리 파라미터, 헤더, 이벤트 데이터 등)은 '절대' 믿지 마. 데이터 타입, 길이, 형식(정규표현식)을 철저히 검사해야 해. 2. 파라미터화된 쿼리 (Parameterized Queries) 사용: SQL/NoSQL 데이터베이스를 사용할 땐, ORM(Object-Relational Mapping)이나 라이브러리가 제공하는 바인딩 기능을 꼭 사용해. 사용자 입력이 쿼리 구조를 바꾸지 못하게 막아줘. 3. 실행 환경 격리: OS 명령어 실행이 꼭 필요하다면, 최소한의 권한을 가진 격리된 환경에서 실행하고, 입력값에 허용된 문자만 포함되도록 화이트리스트 기반으로 필터링해야 해.
// ❌ 나쁜 예: SQL 인젝션에 취약한 코드 (Node.js)
const userId = event.queryStringParameters.id; // 사용자 입력: '1 OR 1=1'
const query = `SELECT * FROM users WHERE id = ${userId}`; // 으악!
db.query(query, ...);
// ✅ 좋은 예: 파라미터화된 쿼리 사용
const userId = event.queryStringParameters.id;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId], ...); // 안전!
S2: 인증 실패 (Broken Authentication) - "넌 누구니? 신분증 좀 보여줘."
서버리스는 상태를 저장하지 않는(Stateless) 특성 때문에, 모든 요청마다 사용자가 누구인지, 그리고 그 요청이 유효한지를 확인해야 해. 전통적인 세션 기반 인증보다는 JWT(JSON Web Tokens) 같은 토큰 기반 인증이 주로 사용되지.
* 취약한 API 엔드포인트: API Gateway에서 엔드포인트를 만들 때, 인증 설정을 깜빡하고 'Public'으로 열어두는 실수가 잦아. 이렇게 되면 누구든 해당 함수를 마음대로 호출할 수 있게 돼. * JWT 토큰 관리 미흡: 토큰 서명에 사용되는 비밀키(Secret Key)가 코드에 하드코딩되거나 유출되는 경우, 해커가 유효한 토큰을 마음대로 만들어낼 수 있어. 또한, 토큰의 만료 시간을 너무 길게 설정하거나, 토큰 탈취 시 이를 무효화할 방법이 없다면 문제가 될 수 있지.
🛡️ 방어 전략:
1. API Gateway Authorizer 활용: AWS API Gateway를 쓴다면, 내장된 인증/인가 기능을 적극적으로 활용해야 해. * Cognito User Pools: 사용자 회원가입, 로그인, 토큰 발급/검증을 손쉽게 처리해 줘. * Lambda Authorizer: 커스텀 인증 로직이 필요할 때 사용. 헤더에 담겨 온 토큰을 검증하는 별도의 Lambda 함수를 만들어 연결할 수 있어. * IAM Roles: AWS 서비스 간의 통신이나 내부용 API에 적합. AWS 서명을 통해 요청을 인증해. 2. 강력한 토큰 정책: JWT 시크릿 키는 AWS Secrets Manager나 Parameter Store 같은 안전한 곳에 보관하고, 코드에서 동적으로 불러와 사용해야 해. 토큰 만료 시간은 가능한 짧게 설정하고, Refresh Token 메커니즘을 구현하는 것이 좋아.
S3: 민감 데이터 노출 (Sensitive Data Exposure) - "비밀번호를 포스트잇에 써 붙여놓다니!"
API 키, 데이터베이스 접속 정보, 서드파티 서비스 인증서... 이런 민감 정보들을 어떻게 관리하고 있어? 혹시 코드에 그대로 박아두거나, 환경 변수에 평문으로 저장하고 있지는 않아? 😱
서버리스 환경에서는 함수 코드나 설정 파일이 유출될 경우, 그 안에 있던 민감 정보가 고스란히 노출될 수 있어. 특히 환경 변수는 디버깅 로그에 실수로 찍히는 경우가 많아서 위험해.
🛡️ 방어 전략:
1. Secrets Management 서비스 사용: AWS Secrets Manager, AWS Systems Manager Parameter Store (SecureString 타입), HashiCorp Vault, Google Secret Manager 같은 전문 서비스를 사용해. 이 서비스들은 민감 정보를 암호화해서 저장하고, IAM과 연동하여 접근 제어를 할 수 있게 해줘. 함수는 실행 시점에 필요한 권한을 가지고 이 서비스에 요청해서 비밀 정보를 동적으로 가져와야 해. 2. 전송 중 데이터 암호화 (Encryption in Transit): 모든 API 통신은 HTTPS/TLS를 사용해서 암호화해야 해. API Gateway에서 'Require TLS' 옵션을 활성화하는 건 기본 중의 기본! 3. 저장 데이터 암호화 (Encryption at Rest): S3, DynamoDB, RDS 등 데이터를 저장하는 모든 서비스에서 서버 측 암호화 옵션을 활성화해야 해.
`.env` 파일이나 설정 파일에 비밀 키를 넣어두고 실수로 `git push` 하는 순간, 그 정보는 전 세계에 공개된다고 봐도 무방해. Public 리포지토리는 물론이고, Private 리포지토리라도 안전하지 않아. 항상 `.gitignore`에 설정 파일을 추가하고, 비밀 정보는 외부 Secrets Management 서비스를 통해 관리하는 습관을 들여야 해.
S5: 접근 제어 실패 (Broken Access Control) - "여긴 어떻게 들어오셨어요?"
인증(Authentication)이 "당신은 누구인가요?"를 확인하는 과정이라면, 인가(Authorization)는 "당신이 이 일을 할 권한이 있나요?"를 확인하는 과정이야. 서버리스에서는 이 '인가' 문제가 훨씬 더 복잡하고 중요해.
* 과도한 IAM 권한: 앞서 말했듯이, 함수에 필요 이상의 권한을 주는 게 가장 흔한 문제야. S3의 특정 버킷, 특정 폴더에만 접근해야 하는 함수에 `s3:*` 권한을 줘버리는 식이지. * 부적절한 API Gateway 설정: API Gateway에서 `GET /items/{itemId}` 같은 엔드포인트를 만들었을 때, 로그인한 사용자가 다른 사용자의 `itemId`를 넣어도 조회가 되는 경우가 있어. "A 사용자는 A의 아이템만 볼 수 있다"는 비즈니스 로직 레벨의 접근 제어가 누락된 거지.
🛡️ 방어 전략:
1. 최소 권한의 원칙 (PoLP) 준수: 모든 함수에 대해 개별 IAM 역할을 생성하고, 그 함수가 수행해야 할 작업에 필요한 '최소한의' 권한만 부여해야 해. 예를 들어, DynamoDB의 특정 테이블에 아이템을 추가만 하는 함수라면 `dynamodb:PutItem` 권한만, 그것도 특정 테이블 리소스에 대해서만 허용해야 해. 2. 함수 코드 내에서 권한 검증: API Gateway에서 1차적으로 인증을 통과했더라도, 함수 코드 내에서 2차적으로 비즈니스 로직에 맞는 권한 검증을 수행해야 해. JWT 토큰에 포함된 사용자 ID(`sub` 클레임 등)를 확인하고, 요청된 리소스의 소유자가 맞는지 확인하는 로직이 반드시 필요해.
// ✅ 좋은 예: IAM Policy에서 최소 권한 부여하기 (JSON)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/MyUserTable"
}
]
}
// ✅ 좋은 예: 함수 코드에서 소유권 확인하기 (Node.js)
const requestedItemId = event.pathParameters.itemId;
const requesterUserId = event.requestContext.authorizer.claims.sub; // JWT에서 추출한 사용자 ID
const item = await db.getItem({ id: requestedItemId });
if (item.ownerId !== requesterUserId) {
return { statusCode: 403, body: 'Forbidden' }; // 내 거 아니면 거부!
}
S6: 보안 설정 오류 (Security Misconfiguration) - "원래 기본값이 그런 걸 어떡해..."
클라우드 서비스는 기능이 워낙 많고 복잡해서, 개발자가 모든 보안 설정을 완벽하게 챙기기 어려워. 서비스의 기본 설정(Default) 값이 보안에 취약한 경우도 있고, 실수로 중요한 보안 옵션을 비활성화하기도 해.
* Public S3 버킷: 가장 유명한 사례지. S3 버킷을 만들 때 실수로 '모든 퍼블릭 액세스 차단' 옵션을 해제해서, 버킷 안의 모든 파일이 전 세계에 공개되는 끔찍한 사고가 끊이지 않고 있어. * 상세한 에러 메시지 노출: 함수에서 에러가 발생했을 때, 스택 트레이스(Stack Trace)나 시스템 내부 정보가 담긴 에러 메시지를 사용자에게 그대로 반환하는 경우. 해커에게 시스템 구조에 대한 힌트를 주는 꼴이야. * CORS 설정 오류: API Gateway에서 CORS(Cross-Origin Resource Sharing) 설정을 할 때, 귀찮다는 이유로 `Access-Control-Allow-Origin` 헤더를 `'*'`로 설정하는 경우가 많아. 이렇게 되면 어떤 웹사이트에서든 내 API를 호출할 수 있게 되어서, 의도치 않은 요청이 발생할 수 있어.
🛡️ 방어 전략:
1. IaC (Infrastructure as Code) 활용: AWS CloudFormation, Terraform, Serverless Framework 같은 도구를 사용해서 인프라 구성을 코드로 관리해야 해. 이렇게 하면 보안 설정을 리뷰하고, 일관성 있게 적용하기가 쉬워져. "클릭 몇 번으로 대충 만들자"는 생각은 금물! 2. 자동화된 보안 점검 도구 사용: AWS Trusted Advisor, AWS Security Hub 같은 서비스를 활용하거나, `Checkov`, `tfsec` 같은 오픈소스 도구를 CI/CD 파이프라인에 통합해서 배포 전에 자동으로 보안 설정 오류를 찾아내야 해. 3. 에러 핸들링: `try-catch` 블록으로 모든 예외를 처리하고, 사용자에게는 "서버 내부 오류가 발생했습니다." 같은 일반적인 메시지만 보여줘. 상세한 오류 정보는 CloudWatch 같은 로깅 시스템에만 기록해야 해.
S9: 알려진 취약점이 있는 구성 요소 사용 - "최신 버전으로 업데이트하는 게 그렇게 귀찮았니?"
현대 애플리케이션 개발은 '만든다'기보다 '조립한다'에 가까워. 수많은 오픈소스 라이브러리와 프레임워크를 가져다 쓰지. `package.json` 파일만 봐도 의존성 목록이 어마어마하잖아.
문제는 이 라이브러리들에서 계속해서 새로운 보안 취약점이 발견된다는 거야. 내가 사용하는 라이브러리의 특정 버전에 심각한 원격 코드 실행 취약점이 발견됐는데, 이걸 모르고 계속 사용한다면? 해커에게 우리 집 현관문 비밀번호를 알려주는 것과 똑같아.
🛡️ 방어 전략:
1. SCA (Software Composition Analysis) 도구 사용: `npm audit`, `yarn audit` 같은 패키지 매니저 내장 명령어를 주기적으로 실행하는 건 기본이야. 더 나아가서, GitHub의 Dependabot이나 Snyk, WhiteSource 같은 전문 SCA 도구를 CI/CD 파이프라인에 연동해야 해. 이런 도구들은 새로운 취약점이 발견되면 자동으로 알려주고, 심지어 보안 패치가 포함된 버전으로 업데이트하는 Pull Request를 자동으로 생성해주기도 해. 2. 불필요한 의존성 제거: 프로젝트를 주기적으로 리뷰해서 더 이상 사용하지 않는 라이브러리는 과감하게 삭제해야 해. 의존성이 적을수록 관리 포인트도, 잠재적 위험도 줄어들어.
S10: 불충분한 로깅 및 모니터링 - "CCTV 없는 금은방"
보안 사고는 100% 막을 수 없어. 중요한 건 사고가 발생했을 때 얼마나 빨리 인지하고, 원인을 파악하고, 대응하느냐야. 그런데 만약 아무런 기록(로그)이 없다면? 이건 마치 CCTV도 경비원도 없는 금은방과 같아. 도둑이 들어와도 언제, 누가, 어떻게 훔쳐 갔는지 알 길이 없는 거지.
* 로깅 비활성화: 비용을 아끼려고, 혹은 귀찮아서 API Gateway나 Lambda 함수의 로깅을 꺼두는 경우가 있어. * 불충분한 로그 내용: 로그를 남기긴 하는데, "함수 실행됨" 같은 의미 없는 내용만 기록하는 경우. 누가(IP 주소, 사용자 ID), 언제, 무엇을(API 엔드포인트, 파라미터), 어떻게(성공/실패 여부, 에러 코드) 했는지에 대한 정보가 없으면 무용지물이야.
🛡️ 방어 전략:
1. 모든 것을 기록하라: AWS 환경이라면 API Gateway 접근 로그, Lambda 실행 로그, WAF 로그, CloudTrail(API 호출 기록) 등 가능한 모든 로그를 활성화하고, CloudWatch Logs에 중앙 집중적으로 수집해야 해. 2. 구조화된 로깅 (Structured Logging): 로그를 그냥 텍스트로 남기지 말고, JSON 형식처럼 키-값 쌍으로 구조화해서 기록하는 게 좋아. 이렇게 하면 나중에 로그를 검색하고 분석하기가 훨씬 쉬워져. 3. 모니터링 및 알림 설정: CloudWatch Alarms 같은 기능을 사용해서 의심스러운 활동에 대한 알림을 설정해야 해. 예를 들어, 특정 시간 동안 4xx(클라이언트 에러) 또는 5xx(서버 에러) 응답이 급증하거나, 특정 함수의 실행 시간이 비정상적으로 길어지거나, 특정 IAM 사용자의 권한 상승 시도가 감지되면 즉시 슬랙이나 이메일로 알림을 받도록 설정해야 해.
서버리스는 여러 함수와 서비스가 체인처럼 엮여서 하나의 요청을 처리하는 경우가 많아. 이럴 때 어디서 병목이 생기는지, 어디서 에러가 났는지 추적하기가 정말 어렵지. AWS X-Ray 같은 분산 추적 서비스를 사용하면, 요청 하나가 어떤 경로를 거쳐 처리되는지 시각적으로 보여줘서 디버깅과 성능 분석에 큰 도움이 돼. 보안 관점에서도 비정상적인 요청 흐름을 파악하는 데 유용하게 쓰일 수 있어.
Chapter 3. 실전! 서버리스 보안 아키텍처 구축하기 (AWS Lambda 중심으로)
자, 이제 이론은 충분히 익혔으니 실전으로 들어가 보자! "그래서 이 모든 걸 어떻게 조합해서 안전한 시스템을 만드는데?" 라는 질문에 답을 줄게. 가장 대중적인 AWS Lambda를 중심으로, 사용자가 API를 호출해서 데이터를 가져가는 간단한 시나리오를 통해 보안 아키텍처의 각 요소를 살펴보자.
1. 최전방 방어선: AWS WAF (Web Application Firewall) 🛡️
모든 요청이 우리 시스템에 도달하기 전에 가장 먼저 만나는 방패야. WAF를 API Gateway 앞에 붙여주면, 흔히 알려진 웹 공격 패턴(SQL 인젝션, XSS, 악성 봇 등)을 가진 요청들을 미리 걸러낼 수 있어. AWS가 미리 만들어 둔 규칙 세트(Managed Rule Sets)를 적용하는 것만으로도 상당한 효과를 볼 수 있지. 특정 IP 주소를 차단하거나, 특정 국가로부터의 접근을 막는 등 커스텀 규칙도 설정할 수 있어.
2. 관문 & 교통경찰: Amazon API Gateway 🚪
WAF를 통과한 요청은 API Gateway에 도착해. API Gateway는 단순한 요청 전달자가 아니라, 우리 시스템의 관문이자 교통경찰 역할을 하는 아주 중요한 컴포넌트야.
* 인증/인가 (Authentication/Authorization): 앞서 말한 Cognito나 Lambda Authorizer를 통해 "이 요청을 보낸 사람이 유효한 사용자인가? 이 API를 호출할 권한이 있는가?"를 검사해. 여기서 통과하지 못한 요청은 Lambda 함수에 도달조차 못 하지. * 요청 유효성 검사 (Request Validation): Lambda 함수로 넘겨주기 전에 요청의 형식이 올바른지 검사할 수 있어. "이 파라미터는 반드시 포함되어야 해", "이 값은 숫자여야 해" 같은 규칙을 미리 정의해두면, 비정상적인 요청으로 인해 함수가 오작동하는 것을 막을 수 있지. * 스로틀링 및 사용량 제어 (Throttling & Usage Plans): 특정 사용자가 너무 많은 요청을 보내 서비스 전체를 마비시키는 DoS(Denial of Service) 공격을 막기 위해, 사용자별 또는 API 키별로 분당/초당 요청 수를 제한할 수 있어.
3. 핵심 브레인: AWS Lambda & IAM Role ⚙️📜
API Gateway의 모든 검증을 통과한 요청은 드디어 Lambda 함수에 도착해서 비즈니스 로직을 수행해. 여기서 가장 중요한 보안 원칙은 바로 '최소 권한의 원칙'이야.
Lambda 함수는 실행될 때 특정 IAM Role(역할)의 권한을 부여받아. 이 IAM Role에는 이 함수가 해야 할 일에 필요한 최소한의 권한만 정의되어 있어야 해. 예를 들어, '특정 DynamoDB 테이블에서 데이터를 읽어오는' 함수라면, IAM Role에는 딱 그 테이블에 대한 `dynamodb:GetItem` 권한만 있어야 하는 거지. `dynamodb:*` 나 `s3:*` 같은 와일드카드 권한은 절대 금물이야.
이건 마치 '어벤져스' 팀을 꾸리는 것과 같아. 각 히어로(함수)에게 딱 맞는 능력(권한)만 줘야 세상을 구할 수 있지, 아무 능력이나 다 주면 오히려 빌런이 될 수도 있어. 💥
4. 데이터 저장소: Amazon DynamoDB (with Encryption) 🗄️
함수는 IAM Role이 허락하는 범위 내에서 DynamoDB에 접근해 데이터를 읽거나 써. 이때 데이터베이스 자체의 보안 설정도 중요해.
* 저장 데이터 암호화 (Encryption at Rest): DynamoDB 테이블을 생성할 때, 암호화 옵션을 반드시 활성화해야 해. AWS 소유 키, AWS 관리형 키(KMS), 고객 관리형 키(KMS) 중 선택할 수 있는데, 특별한 규제 요건이 없다면 AWS 관리형 키로도 충분한 보안을 제공해. 이렇게 하면 데이터가 디스크에 저장될 때 자동으로 암호화되어서, 만에 하나 물리 디스크가 유출되더라도 데이터를 읽을 수 없어. * 세분화된 접근 제어 (Fine-Grained Access Control): IAM 정책을 통해 특정 사용자나 역할이 테이블 전체가 아닌 특정 항목(Row)이나 속성(Column)에만 접근하도록 제어할 수도 있어.
5. 비밀 금고 & 감시탑: Secrets Manager & CloudWatch 🔒📊
이 모든 구성 요소를 안전하게 연결하고 감시하는 역할이야.
* AWS Secrets Manager: 데이터베이스 접속 정보나 외부 API 키 같은 민감 정보는 여기에 저장해. Lambda 함수는 실행될 때 IAM 권한을 이용해 Secrets Manager에서 필요한 비밀 정보를 안전하게 가져와서 메모리에서만 사용하고, 코드나 환경 변수에는 절대 남기지 않아. Secrets Manager는 비밀번호를 주기적으로 자동 교체(Rotation)해주는 기능도 제공해서 보안 수준을 한층 더 높일 수 있어. * Amazon CloudWatch: 우리 시스템의 모든 활동을 기록하는 CCTV야. API Gateway의 접근 로그, Lambda 함수의 실행 로그와 에러, IAM을 통한 권한 사용 내역(CloudTrail) 등이 모두 CloudWatch Logs로 모여. 그리고 CloudWatch Alarms를 설정해서 "5분 동안 5xx 에러가 10번 이상 발생하면 즉시 알림" 같은 규칙을 만들어두면, 문제가 생겼을 때 바로 알아차리고 대응할 수 있지.
Chapter 4. 코드 레벨에서 지켜야 할 것들: 시큐어 코딩 in Serverless
아무리 인프라 보안을 튼튼하게 구축했더라도, 결국 비즈니스 로직을 처리하는 코드 자체에 구멍이 있다면 말짱 도루묵이야. "Shift Left"라는 말 들어봤어? 보안을 개발 프로세스의 가장 왼쪽, 즉 코드 작성 단계부터 신경 써야 한다는 뜻이야. 서버리스 환경에서 개발자가 코드 레벨에서 꼭 지켜야 할 시큐어 코딩 원칙들을 알아보자.
1. 입력값 검증: "아무도 믿지 마, 특히 사용자는 더더욱!" 🧐
모든 보안 문제의 시작은 외부 입력을 신뢰하는 것에서부터 비롯돼. API 요청의 본문(body), 쿼리 스트링(query string), 헤더(header), 경로 파라미터(path parameter) 등 함수로 들어오는 모든 데이터는 잠재적인 공격 페이로드라고 생각해야 해.
단순히 값이 비어있는지(null check)만 확인하는 걸로는 부족해. 데이터의 타입, 길이, 형식, 허용 범위까지 아주 꼼꼼하게 검증해야 해. 예를 들어, '나이'를 입력받는 필드라면 숫자인지, 0보다 크고 150보다 작은지 등을 모두 확인해야겠지.
이런 검증 로직을 매번 `if`문으로 짜는 건 너무 번거롭고 실수하기도 쉬워. `Joi`(JavaScript), `Zod`(TypeScript), `Pydantic`(Python) 같은 스키마 기반 유효성 검사 라이브러리를 사용하면, 데이터의 구조와 규칙을 선언적으로 정의하고 손쉽게 검증할 수 있어.
// ✅ 좋은 예: Zod 라이브러리를 사용한 입력값 검증 (TypeScript)
import { z } from 'zod';
const UserSchema = z.object({
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
export const handler = async (event) => {
try {
const body = JSON.parse(event.body);
const validatedData = UserSchema.parse(body); // 여기서 검증 실패하면 에러 발생!
// 검증 통과한 데이터만 사용
await createUser(validatedData);
return { statusCode: 201, body: 'User created' };
} catch (error) {
if (error instanceof z.ZodError) {
return { statusCode: 400, body: JSON.stringify(error.issues) }; // 사용자에게 친절한 에러 메시지
}
// ... 기타 에러 처리
}
};
2. 의존성 관리: "우리 집에 사는 룸메이트, 신원 확인은 하셨나요?" 🕵️
`npm install` 한 줄이면 온갖 신기한 기능이 뚝딱 만들어지는 편리한 세상이지만, 그만큼 위험도 커. 내가 설치하는 패키지가 또 다른 수십 개의 패키지에 의존하고, 그중 하나에라도 취약점이 있다면 내 애플리케이션 전체가 위험에 노출돼.
* CI/CD 파이프라인에 보안 스캔 통합: 코드를 빌드하고 배포하는 CI/CD 파이프라인에 `npm audit --production` 이나 Snyk, Trivy 같은 도구를 사용한 의존성 스캔 단계를 반드시 포함해야 해. 취약점이 발견되면 빌드를 실패시키고 개발자에게 즉시 알리도록 설정해야 하지. * `package-lock.json` 활용: `package-lock.json` (또는 `yarn.lock`) 파일은 현재 설치된 패키지들의 정확한 버전을 기록해 둬. 이 파일을 Git에 함께 커밋하면, 다른 개발자나 빌드 서버에서도 항상 동일한 버전의 패키지를 설치하게 되어 "내 컴퓨터에선 됐는데..." 하는 문제를 막고, 의도치 않은 버전 업데이트로 인한 취약점 유입을 방지할 수 있어. * 정기적인 업데이트: 귀찮더라도 의존성 패키지들은 정기적으로 최신 버전으로 업데이트해주는 게 좋아. GitHub의 Dependabot 같은 봇을 활용하면 이 과정을 자동화할 수 있어.
마치 재능넷에서 실력 있고 검증된 전문가를 찾아 프로젝트를 맡기는 것처럼, 우리가 사용하는 오픈소스 라이브러리도 평판이 좋고, 꾸준히 관리되며, 보안에 신경 쓰는 것을 선택해야 해.
3. 에러 처리: "쉿! 우리 집 사정은 우리만 알자고요." 🤫
코드에서 예외(Exception)가 발생했을 때, 그 상세한 내용을 사용자에게 그대로 보여주는 건 해커에게 시스템 내부 구조에 대한 정보를 떠먹여 주는 것과 같아. 데이터베이스 스키마 정보, 파일 경로, 사용 중인 라이브러리 버전 등이 노출될 수 있거든.
* 일반적인 에러 메시지: 사용자에게는 "요청을 처리하는 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요." 와 같이 모호하고 일반적인 메시지를 반환해야 해. * 상세한 서버 로그: 반면에, 개발자가 원인을 파악할 수 있도록 상세한 에러 정보(에러 메시지, 스택 트레이스, 요청 정보 등)는 CloudWatch 같은 로깅 시스템에만 기록해야 해.
// ❌ 나쁜 예: 스택 트레이스를 그대로 노출
export const handler = async (event) => {
try {
// ... 로직 수행 중 에러 발생
await riskyOperation();
} catch (error) {
return { statusCode: 500, body: error.stack }; // 으악! 내부 정보가 다 보여!
}
};
// ✅ 좋은 예: 에러는 로깅하고, 사용자에게는 일반 메시지 반환
export const handler = async (event) => {
try {
// ... 로직 수행 중 에러 발생
await riskyOperation();
} catch (error) {
console.error('An error occurred!', { // CloudWatch에 상세 로그 기록
error: error.message,
stack: error.stack,
requestId: event.requestContext.requestId
});
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal Server Error' }) // 사용자에게는 일반적인 메시지
};
}
};
4. 리소스 설정: "무한동력은 없어, 과부하를 막아라!" ⏱️
서버리스 함수의 타임아웃(Timeout)과 메모리 설정은 비용과 성능뿐만 아니라 보안과도 직결돼.
* 타임아웃 설정: 함수의 최대 실행 시간을 설정하는 거야. 만약 코드에 버그가 있어서 무한 루프에 빠지거나, 외부 API 호출이 응답 없이 계속 대기하는 상황이 발생하면, 타임아웃이 이 실행을 강제로 중단시켜줘. 이게 없다면 함수가 계속 실행되면서 불필요한 비용을 발생시키고, 서비스 전체의 가용성에 영향을 줄 수 있어. 함수의 평균 실행 시간을 측정해보고, 충분한 여유를 두되 너무 길지 않게 설정하는 게 중요해. (예: 평균 500ms라면 3~5초 정도로 설정) * 메모리 설정: 함수에 할당되는 메모리 크기야. 너무 작게 설정하면 복잡한 작업을 하다가 `OutOfMemory` 에러가 발생할 수 있고, 너무 크게 설정하면 비용이 낭비되지. 특히 파일 처리나 이미지 리사이징 같은 작업을 할 때, 악의적으로 매우 큰 파일을 업로드해서 함수 메모리를 고갈시키는 공격(Denial of Wallet)을 시도할 수도 있어. 따라서 처리할 데이터의 최대 크기를 제한하고, 그에 맞는 적절한 메모리를 할당해야 해.
🏁 결론: 서버리스, 자유에는 책임이 따른다. 쿨한 개발자의 보안 체크리스트
지금까지 정말 긴 여정을 함께 달려왔어. 서버리스가 왜 핫한지부터 시작해서, 전통적인 보안과 뭐가 다른지, 어떤 위협들이 있는지, 그리고 어떻게 막아야 하는지까지. 머리가 좀 아플 수도 있지만, 이것 하나만은 꼭 기억해 줘.
서버리스는 '보안이 필요 없는' 기술이 아니라, '보안의 책임이 개발자에게 더 가까워진' 기술이야. 인프라팀이나 보안팀에 미룰 수 있는 영역이 줄어들고, 우리가 작성하는 코드 한 줄, 우리가 내리는 설정 하나하나가 바로 보안의 최전선이 된 거지. 하지만 이건 부담이 아니라 오히려 기회일 수 있어. 처음부터 안전한 설계를 고민하고, 시큐어 코딩을 습관화한다면, 우리는 훨씬 더 견고하고 신뢰성 있는 서비스를 만들 수 있는 '풀스택'을 넘어선 '시큐어-스택' 개발자가 될 수 있을 거야. 😎
마지막으로, 새로운 서버리스 프로젝트를 시작하거나 기존 프로젝트를 점검할 때 활용할 수 있는 최종 체크리스트를 정리해 봤어. 이걸 보면서 "아, 맞다! 이거 빼먹었네!" 하는 부분이 없는지 꼭 확인해 봐.
[IAM & 접근 제어]
▢ 모든 함수는 각자의 역할에 맞는 최소한의 권한을 가진 IAM Role을 사용하는가?
▢ IAM 정책에 와일드카드(`*`)를 남용하지 않았는가?
▢ 민감한 작업을 수행하는 API는 적절한 인증/인가(Cognito, Lambda Authorizer 등) 절차를 거치는가?
▢ 함수 코드 내에서 리소스 소유권 등 비즈니스 로직 레벨의 접근 제어를 수행하는가?
[데이터 보호]
▢ API 키, DB 접속 정보 등 민감 정보는 Secrets Manager 같은 전문 서비스를 통해 관리하는가?
▢ 코드나 환경 변수에 민감 정보가 하드코딩되어 있지 않은가?
▢ S3, DynamoDB 등 모든 데이터 저장소에 저장 시 암호화(Encryption at Rest)가 활성화되어 있는가?
▢ 모든 외부 통신은 HTTPS/TLS를 통해 암호화(Encryption in Transit)되는가?
[코드 & 의존성]
▢ 외부로부터 들어오는 모든 입력값(HTTP 요청, 이벤트 데이터 등)을 신뢰하지 않고 철저히 검증하는가?
▢ 데이터베이스 쿼리는 파라미터화된 방식으로 안전하게 실행하는가?
▢ CI/CD 파이프라인에 의존성 취약점 스캔(SCA) 도구가 통합되어 있는가?
▢ 상세한 오류 정보는 사용자에게 노출하지 않고, 서버 측 로그에만 기록하는가?
[인프라 & 설정]
▢ S3 버킷 등 클라우드 리소스가 불필요하게 외부에 공개되어 있지 않은가?
▢ API Gateway 앞에 WAF를 적용하여 일반적인 웹 공격을 방어하고 있는가?
▢ API Gateway에 스로틀링(요청 수 제한)이 적절하게 설정되어 있는가?
▢ 인프라 구성은 코드로 관리(IaC)되고 있는가?
[로깅 & 모니터링]
▢ 모든 컴포넌트(API Gateway, Lambda 등)의 로그가 활성화되어 중앙에서 수집되는가?
▢ 비정상적인 활동(에러 급증, 권한 상승 시도 등)에 대한 모니터링 및 알림이 설정되어 있는가?
▢ 로그에 개인정보 등 민감한 데이터가 기록되지 않도록 처리했는가?
이 체크리스트가 너의 든든한 보안 가이드가 되길 바라. 서버리스의 무한한 가능성을 마음껏 누리되, 그 자유에 따르는 책임도 잊지 않는 멋진 개발자가 되자고! 화이팅! 💪
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.