저는 평소 인프라를 구축할때 보안적인 측면은 많이 고려하지 않았는데요.
이번 기회로 인프라, 네트워크를 구축할 때 고려해야할 내용으로 배운 내용을 정리할겸
제가 겪은 악의적인 트래픽 공격
을 겪은 문제 상황과 해결한 과정을 공유해 보겠습니다.
최근 아직 서비스 배포도 하지 않았음에도, 개발중인 서버에
에러 로그와 트래픽이 미친듯이 발생하는 이슈가 발생했습니다.
기존에 어떤 ip
를 통해서 서버로 요청을 보내는지에 대한 모니터링 장치가 구축되어있지 않았기 때문에
일단 급한대로 직접 EC2
에 직접 접속하여 tcpdump
명령어로 패킷 정보를 모니터링 하기로 하였습니다.
악성 요청이 주기적으로 계속 오고 있었기에 급하게 캡쳐한 패킷 정보로도 현 상황에 대한 유의미한 모니터링 정보를 확보할 수 있었습니다.
패킷 dump
파일을 scp
명령어로 제 로컬 컴퓨터에 옮기고 Wireshark
툴로 Http
요청을 확인해보니
.env
,.git/config
같이 중요한 파일의 담긴 경로는 물론이고xxx.php
같은 경로등 정말 다양한 주소로부터 각종 악의적인 요청이 들어오는것을 확인해볼 수 있었습니다 😰
요청이 들어오는 경로는 정말 다양했는데요. 트래픽이 들어오는 서버의 ip 경로들을 적어보면
- 배포한 서버의 도메인 주소
- EC2 프라이빗 ip 주소 (EC2 health check 요청)
- EC2 퍼블릭 IPv4 주소
- Application Load Balnacer의 DNS ip 주소들
43.201.xxxx
207.154.xxxx
15.165.xxxx
13.125.xxxx
...
(nslookup ALB 도메인 주소
명령어로 ALB DNS 주소를 확인할 수 있습니다.)
2번은 AWS가 제공하는 내부적인 ec2 health check
트래픽이니 논외로 하고
1,3,4번의 경로로 오는 악의적인 요청은 별도의 대응이 필요해 보였습니다.
특히나 이런 공격으로 크게 두가지의 큰 손실을 유발할 여지가 있었는데요
- 서버의 중요한 파일이 외부로 유출됨
- 서버에 요청을 마구잡이로 보내 서버에 장애가 일어남
현재 서버에서 중요한 파일을 API
엔드포인트로 열어놓은 경우는 없었기에
1은 당장은 실제 피해로 이어질 여지가 없어 보였지만,
2는 서버로 서비스 운영에 직접적인 장애로 이어질 여부가 있어 보였습니다.
정상적인 트래픽 구조라면 위의 그림처럼
유저가 도메인 주소를 통해 api 요청
-> 도메인 주소의DNS 서버
로 요청 전달
->DNS 서버
의 ip주소와 연동된AWS Route 53
으로 호스팅
->ALB(Application Load Balancer)
로 호스팅
->ALB
에서 매핑해 놓은대상 그룹(target group)
으로 호스팅
(EC2
로 호스팅하면서http
요청은https
요청으로 리다이렉션후 요청 인증서 보안 처리)
->Target group
에서 매핑한EC2 포트(8080)
로 트래픽 전달
(Target group
에서EC2
혹은 외부IP 주소
로 호스팅 가능)
-> EC2 API 서버에서 요청 처리
위의 순서로 트래픽이 전달되어야 정상인데도 불구하고
외부에서 ALB
로 직접 요청을 보내거나 더 심하게는 EC2
로 직접 트래픽 요청을 허용하는 상황이 벌어지고 있었습니다.
이런 상황에서 악의적인 트래픽을 효율적으로 관리하기 위해서는 두가지 개선이 필요하다고 생각했습니다.
- 요청이 들어오는 입구를 도메인을 통한 요청으로만 중앙화하자
- 중앙화된 요청을 필터링을 통하여 관리하자
우선 여러 퍼널로 들어오는 요청들은 관리하기 너무 어려우니
EC2 퍼블릭 IPv4 주소
로 직접적으로 들어오는 요청과 ALB IP 주소
로 호출하는 요청을
허용하지 않는 보안 정책을 도입하는게 가장 손쉽게 적용할 수 있는 방법이라고 생각했습니다.
그리고 실제로, AWS 관리 콘솔에서 클릭 몇번으로 보안 그룹(Security Group)
을 생성하고
인바운드(Inbound) 규칙
관리를 하면 간단하게 설정이 가능한 인프라를 지원하고 있기 때문에
설정을 진행했습니다.
기존에는 ALB
, VPC
에서 사용하던 보안 그룹을 하나로 통일해서 사용하느라
VPC
에서 허용하면 안되는 Https(443)
, http(80)
포트가 허용되어 있어서
EC2 퍼블릭 IPv4 주소
로 직접적인 요청이 가능했습니다.
VPC
보안 그룹 설정EC2들과, RDS들이 모여있는 VPC
에 적용하는 보안그룹을 생성하여
인바운드 규칙으로
스프링 서버가 요청을 받는 8080 포트
,
Postgres RDS 5432 포트
,
EC2 ssh 연결을 위한 22번 포트
만 허용하도록 설정을 허용했고,
8080 포트
는 ALB 보안그룹 아이디
를 매핑해서 ALB
에서 오는 Https
요청만 요청을 허용하도록 설정했습니다.
Postgres RDS
, SSH
포트는 각각 자체적으로
보안정책(RDS:접속 아이디,비밀번호 검증
), (SSH: pem 키 사용
)이 존재하므로
개발 편의성을 위해서 따로 ip 제약을 걸지 않았습니다.
ALB
보안 그룹 설정ALB 보안 그룹
과 VPC 내부 보안 그룹
분리를 진행하였습니다.
이제 트래픽을 중앙화 했으니 전체 트래픽중에서
어떤 요청을 악의적인 트래픽으로 처리하고 걸러낼지 고민이 필요한 상황입니다.
제일 간단한 방법은 AWS WAF 서비스를 이용하는 것입니다.
WAF
솔루션은 악성 요청을 보내는 IP를 자동으로 블락하고 허용할 요청 경로를 설정하는 등의 여러기능을 제공합니다.
하지만, 위 방법은 비용이 청구되는 AWS의 솔루션이기 때문에
영세한 프로젝트를 진행하는 입장에서 바로 도입하기에 부담이 되었습니다.
악성 요청을 필터링하는 요구사항을 만족하고 금액이 청구되지 않는 다른 방법을 고민해보게 되었습니다.
악성 트래픽을 보낸 IP 정보를 패킷 정보에서 확인해보면
- 요청 보내는 악의적인 API 엔드포인트가 베리에이션이 끝이 없다.
- 요청 보내는 IP 주소를 계속 바꿔서 호출하기 때문에 어떤 IP가 악성 유저인지 판별이 어렵다.
AWS가 제공하는 별도의 솔루션을 사용하지 않은 상황에서 특정 IP가 악성 유저인지 식별후 블락하는 로직을 추가하기에는 어려움이 있다고 생각했습니다.
그래서, 제가 생각한 솔루션은 API 서버의 엔드포인트만 요청을 허용하도록 필터링 규칙을 적용하자
였습니다.
그 이유로, 어떤 ip를 블락할지 결정하기에 어려운 상황
이며 어떤 요청을 보내는지 그 패턴을 식별하기 어려운 상황
에서는
개발한 서버의 API 엔드포인트만 허용하고 나머지 요청을 다 막아버리는 보안정책
만 도입하더라도
적은 공수 대비 유의미한 효과를 볼 수 있을 것
이라는 생각이 들었기 때문입니다.
이러한 보안 정책을 실제로 적용할 솔루션으로 처음으로 떠올린 것은 Nginx
에서 필터링 규칙을 적용하는 것이었습니다.
이미 기존에 설정되어 있는 CI/CD
구조로 Github Actions
를 사용하고 있었기에
배포할때마다 필터링 규칙을 동적으로 적용하는 다음의 솔루션을 생각했습니다.
- 서버 프로젝트
전체 API 엔드포인트 리스트
를 응답으로 출력하는API
를 구현한다.Github Actions
에서Spring Boot
소스코드를 빌드한다.- 빌드한
JAR
파일을 실행해서 1의API
로 부터전체 API 리스트
를 가져온다.- 3의 리스트를 배포전
Nginx
필터링 규칙에 적용한다.- 빌드한
JAR
파일을EC2
에 배포한다.
위의 과정을 거치면 허용해야하는 API
엔드포인트가 생겨날 때 마다
일일히 추가해줄 필요 없이 배포시에 자동으로 Nginx
에 보안 정책을 도입할 수 있을 것이라고 생각했습니다.
하지만, 이 방식은 스케일 아웃(Scale Out)
을 통해 인프라 구조가 조금만 바뀌면
추가된 Nginx
들을 모두 관리해줘야 한다는 문제가 생길수도 있는 방법이었습니다.
개발하고 있던 프로젝트가 추후 스케일 아웃(Scale Out)
까지 고려해서 만들고 있던 상황이었기 때문에
이런 측면도 감안하여 좀 더 공수가 덜 들어가면서 같은 효과를 보는 방법이 있을지 고민해보게 되었습니다.
ALB
에서 악성 요청 필터링하기그러다 고민중에 나온 아이디어로 2번에서 Nginx
가 하는 필터링을ALB
에서 하도록 설정하는 방법이었습니다.
스케일 아웃
을 진행한다고 하더라도 ALB
를 사용한 인프라 구조는 그대로 가져갈 예정이었기 때문에
ALB 의 보안그룹 정책을 명령어를 통해 동적으로 추가하고 삭제하는 방법
을 찾아보게 되었고,
AWS CLI 를 사용하여 ALB를 동적으로 관리하는 방법을 제공하고 있음을 확인했습니다.
- 서버 프로젝트
전체 API 엔드포인트 리스트
를 응답으로 출력하는API
를 구현한다.Github Actions
에서Spring Boot
소스코드를 빌드한다.- 빌드한
JAR
파일을 실행해서 1의API
로 부터전체 API 리스트
를 가져온다.- 3의 리스트를
Github Actions
스크립트상에서AWS CLI
를 사용하여ALB
필터링 규칙에 적용한다.- 빌드한
JAR
파일을EC2
에 배포한다.
위의 두번째 방법에서 4의 부분만 Nginx
-> ALB
로 바꿔서 처리하면 됩니다!
- 서버 프로젝트
전체 API 엔드포인트 리스트
를 응답으로 출력하는API
를 구현한다.
@RestController
@RequestMapping("/endpoints")
public class EndpointController {
@Autowired
@Qualifier("requestMappingHandlerMapping")
private RequestMappingHandlerMapping handlerMapping;
@Operation(summary = "배포시 ALB 보안 규칙 필터링 endpoint 출력용 API")
@GetMapping
public List<String> getEndpoints() {
List<String> endpoints = new ArrayList<>();
handlerMapping.getHandlerMethods().forEach((requestMappingInfo, handlerMethod) -> {
requestMappingInfo.getPathPatternsCondition().getPatternValues().stream()
.filter(pattern -> pattern.startsWith("/"))
.filter(pattern -> !pattern.equals("/")) // '/' 경로로 임의로 쏘는 요청이 너무 많이 들어와서 block
.forEach(endpoints::add);
});
// swagger-ui endpoint
endpoints.add("/swagger-ui/*");
return endpoints.stream()
.distinct()
.sorted()
.toList();
}
}
위의 컨트롤러 메서드를 호출하여 API 엔드포인트를 리스트 형태로 뽑아내는것이 가능합니다.
Github Actions
에서Spring Boot
소스코드를 빌드한다.- 빌드한
JAR
파일을 실행해서 1의API
로 부터전체 API 리스트
를 가져온다.
- name: Run Spring Boot application # Spring Boot 애플리케이션 실행 후 API 엔드포인트 추출
run: |
java -jar deploy/*.jar --spring.profiles.active=ci &
sleep 15 # 애플리케이션 시작 대기
- name: Extract API endpoints
run: |
curl http://localhost:8080/endpoints > deploy/api-endpoints.txt
pkill java
위의 코드를 Github Actions
실행 스크립트의 JAR
파일 빌드 이후에 추가하여 API
엔드포인트를
deploy/api-endpoints.txt
로 변환하는 과정을 추가합니다.
- 3의 리스트를
Github Actions
스크립트상에서AWS CLI
를 사용하여ALB
필터링 규칙에 적용한다.
# update alb rules
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Configure AWS CLI path
run: |
echo 'export PATH=$PATH:/usr/local/bin/aws' >> $GITHUB_ENV
- name: Update ALB rules
run: |
AWS_CLI_PATH=$(which aws)
echo "AWS CLI Path: $AWS_CLI_PATH"
HTTPS_LISTENER_ARN="${{ secrets.HTTPS_LISTENER_ARN }}"
TARGET_GROUP_ARN="${{ secrets.TARGET_GROUP_ARN }}"
# 기존 HTTPS 규칙 삭제 (기본 규칙 제외)
RULES=$($AWS_CLI_PATH elbv2 describe-rules --listener-arn $HTTPS_LISTENER_ARN --query 'Rules[?Priority!=`default`].RuleArn' --output text)
for RULE_ARN in $RULES
do
$AWS_CLI_PATH elbv2 delete-rule --rule-arn $RULE_ARN
done
# API 엔드포인트를 ALB 규칙 조건 형식으로 변환 : users/{userId} -> users/* 변환
jq -r '.[] | select(. != "/swagger-ui.html") | gsub("\\{[^}]+\\}"; "*")' deploy/api-endpoints.txt > alb_paths.txt
# 사용 가능한 다음 우선순위 찾기
NEXT_PRIORITY=$($AWS_CLI_PATH elbv2 describe-rules --listener-arn $HTTPS_LISTENER_ARN --query 'max(Rules[?Priority!=`default`].Priority)' --output text)
if [ "$NEXT_PRIORITY" == "None" ]; then
NEXT_PRIORITY=1
else
NEXT_PRIORITY=$((NEXT_PRIORITY + 1))
fi
# 각 경로에 대해 개별 규칙 생성
while IFS= read -r PATH
do
$AWS_CLI_PATH elbv2 create-rule \
--listener-arn $HTTPS_LISTENER_ARN \
--priority $NEXT_PRIORITY \
--conditions '[{"Field": "path-pattern", "PathPatternConfig": {"Values": ["'"$PATH"'"]}}]' \
--actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN
NEXT_PRIORITY=$((NEXT_PRIORITY + 1))
done < alb_paths.txt
# 마지막 규칙으로 403 Forbidden 규칙 추가
$AWS_CLI_PATH elbv2 create-rule \
--listener-arn $HTTPS_LISTENER_ARN \
--priority $NEXT_PRIORITY \
--conditions '[{"Field":"path-pattern","PathPatternConfig":{"Values":["/*"]}}]' \
--actions Type=fixed-response,FixedResponseConfig='{StatusCode=403,ContentType="text/plain",MessageBody="Forbidden"}'
echo "ALB HTTPS rules updated successfully"
- 빌드한
JAR
파일을EC2
에 배포한다.
5번에서는 기존에 사용하던 EC2 배포 과정을 사용해서 배포를 진행합니다.
위 코드로 배포를 진행하면 배포시마다 요청을 허용할 API 엔드포인트가 생기는 것을 확인할 수 있습니다.
그리고 마지막으로 허용된 경로가 아닌 모든 요청은
403 Forbidden
을 반환하게 됩니다.
위의 솔루션을 도입해서
평균 분당 15,000번
가량 튀어 오르던 악성 트래픽을 필터링하여분당 100건 미만
으로 개선하는 것에 성공했습니다.
API
엔드포인트로 허용한 주소 외의 요청은 전부 필터링 된 것을 확인할 수 있습니다.
요청 보낸
ip
주소도 도메인 주소로 오는 요청만 허용된 것을 확인해 볼 수 있습니다.
이번 문제 상황을 개선한 과정을 요약하면 다음과 같습니다.
- 문제가 되는 트래픽 패킷 정보를 모니터링 하여 무엇이 문제인지 파악
- 서버로 오는 요청을
도메인을 통한 요청
으로 중앙화- 문제가 되는 랜덤한
API 엔드포인트
를 서버에서 개발한 리스트만 허용ALB
에서 3의 과정을 자동으로 필터링되게끔Github Actions
배포 스크립트에 적용- 개선한 결과를 트래픽 패킷 정보를 모니터링하여 수치로 확인
대단한 알고리즘을 작성하지도, 유행하는 신규 프레임워크를 사용한 것도 아니지만
생각보다 좋은 결과를 얻었고 어렵게만 생각되는 보안도 적당한 노력을 들여서 개선할 수 있다는 것을 배워서
의미있었던 시간이었습니다. 😄