- BOJ 30029
- 수열과 쿼리 43
- hhs2003
- 2023 SW - IT Contest
- 알고리즘
- CodeForces
- 세그먼트 트리
- 컴퓨터융합학부
- solved.ac
- BOJ 30026
- BOJ 31226
- 2023 Engineering Pair
- 누텔라트리(hard)
- dx dy
- 오일러투어트리
- 느리게 갱신되는 세그먼트 트리
- K8s
- BOJ 30028
- Delaunay triangulation
- voronoi diagram
- 27173
- 2025acpc
- BOJ 30027
- boj23054
- 충남대학교 2023 SW - IT
- fortune's algorithm
- 27114
- BOJ17139
- boj 30788
- 백준
황현석 일지
클라우드 서버 구축 일지 02 - Networking Proxy 본문
이번에도 여전히 네트워크와 맨날 싸움중이다.
보안 사고는 자기가 일어나고 싶지 않아도 일어날 수 있는게 보안사고이다. 사용자들의 안정성, 네트워킹의 보안을 검토하고 로그를 적절히 수집하기 위해, 전역에서 Https를 복호화 하고, 개별 사용자들에게 Proxy 시키는 아키텍처를 구상했다.
그리고, 각 http 요청을 복제하여, 기존 백엔드 시스템의 /api/intercept 로 모든 내용을 Middleware식으로 보낼 것이고, AWS의 WAF같은 개념을 도입하여, Rule Based Defense 비슷한 매커니즘으로 사전에 보안사고를 대응하고 해킹시도가 있었다는 것을 알 수 있게 설계하려고 한다.
그 이전, 우선 네트워크와 관련하여 트러블이 있었다. 네트워크 개념은 많이 사용하지 않는 이상 계속 햇갈리기 마련이다.
공부한 내용이다.
우선, K3s를 사용하고 있으므로, K3s의 네트워크 흐름에 대해 말하면 간단하다.
1. K3s의 kube-system namespace의 Traefik Pod의 Traefik을 Ingress Controller로 띄운다.
2. Traefik은 Control Plane 의 Ingress 오브젝트를 감시, 호스트 헤더와 경로를 통해, 알맞은 service로 보냅니다.
3. Service를 통해, 알맞은 Pod로 보냅니다.
사실 이건 진짜 개념을 추상화 해놓은 것이고, 사실은 Service라는건 하나의 단위 이고, 이는 kube-proxy엔진이 담당한다. 하지만 kube-proxy 는 또 Proxy역할을 하지는 않는다.
Service객체의 port와 targetport를 잇는 것은 리눅스 커널의 netfilter 체인을 사용한다. 패킷이 사용자 공간으로 올라오지 않고 커널에서 바로 라우팅되므로 오버헤드가 적다. iptables를 관리한다는 이야기이다.
암튼, 백엔드 VM하나로 80번 포트로 왓을 때, 적절하게 라우팅 하기 위해서는,
kind: Service
apiVersion: v1
metadata:
name: vm-web-service
namespace: cloud-admin
spec:
ports:
- port: 80 # Ingress가 바라보는 포트
targetPort: 8080 # 실제 Pod(컨테이너) 내부 포트
selector:
vm.kubevirt.io/name: my-cloud-vps
다음과 같이, Service를 또 등록을 해주어야 한다.
8080은 바로 백엔드가 받고 있는 포트이고, Service 종류의 80을 8080으로 my-cloud-vps로 가게하는 테이블을 등록해준다.

그 다음은, Ingress 타입의 선언 파일을 만들어, 방금 만든 service의 80번 포트로, 보내버리면 된다.
이러면 잘 되야 하는데 잘 안됐다.
원인은 또 강력한 Network Policy에 있었다. 너무 많은 ingress를 차단하고 있었기에, 다음과 같이

다음과 같이, kube-system namespace의 ingress를 허용하여, http 통신을 허용했다.
Network는 Stateful하기 때문에, Ingress만 설정하면 편하게 진행할 수 있다.
자 이제 외부에서 백엔드로 잘 통신이 되는걸 확인했다.
이제는 전역 tls를 설정하고, 그걸 먼저 백엔드에 우선적으로 적용하는 과정을 할것이다.
음... 일단 TLS 인증서를 받아야하는데, 사긴 좀 그러니까 Let's Encrypt를 이용해서 인증서를 받았다.
잠깐 K3s서버를 내리고, 대충

저런식으로 인증을 했는데, 생각해보니까, hy3on.site에 대해서만 인증을 할게 아니라, *.hy3on.site에 대해서 인증을 시도해야한다.
음.. 그래서 ㅠㅠ 직접 와일드 카드를 명시해서 내가 DNS 설정 권한이 있는지 확인하기 위해, 레코드를 직접 달아주는 행동을 하여 인증서를 얻어냈다.





인증서를 가져 왔으니, 이제 k3s의 secret 종류의 컴포넌트를 만들고, 이를 tls인증서로 사용하게 연결하면 된다.
라고 생각했는데, 저런식으로 sudo cp -r 을 해서 복사를 해오니 다음과 같이, Symbolic Link만 복사되었다.

뒤져보다가 왜 그런지는 모르겠고 cp를 -L옵션을 줘서 해서 링크를 타고 복사를 하게 만들었다... 힘들다 진짜로...

다음과 같이, global-tls-secret을 만들고,
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: kube-system
spec:
defaultCertificate:
secretName: global-tls-secret
다음과 같이 traefik 의 TLSStore를 사용하여, 기본적인 인증서를 등록하였습니다. 따라서, Traefik을 사용하는 Ingress의 TLS에 인증서를 따로 설정하지 않으면 다음 인증서가 기본으로 적용될 것입니다.

자 이제 MiddleWare를 추가해서, 모든 패킷을 가로채고, 나중에 후처리를 해보자. 일단 백엔드에는 /api/intercept 를 추가했다.

다음과 같이 우선, Middleware 타입의 interceptor라는 컴포넌트를 만듭니다.
이 Middleware를 정의해놓고 이제 Ingress의 Anotation에 Middleware를 사용하라고 적어만 놓으면 됩니다.
forwardAuth는 Middleware로 보낼 정확한 endpoint를 서술해야합니다.
경로는 <service-component-name>.<namespace>.svc.cluster.local 이 됩니다.
따라서, 저는 정확하게 저 Url이 Backend의 /api/intercept를 만들어 놧기에 보냅니다.
package controllers
import (
"regexp" // Added for regular expressions
"sync"
"github.com/gin-gonic/gin"
)
// securityEngine: 보안 검사 엔진 구조체
// 정규식 및 보안 규칙을 관리합니다.
type securityEngine struct {
sqlInjectionPatterns []*regexp.Regexp
xssPatterns []*regexp.Regexp
pathTraversal *regexp.Regexp
obfuscationPatterns []*regexp.Regexp // 난독화/이상 문자열 패턴
}
type Interceptor struct {
securityEngine *securityEngine // 보안 엔진 추가
}
var (
interceptor *Interceptor
onceInter sync.Once
)
// GetInterceptor: Interceptor 싱글톤 인스턴스 반환
func GetInterceptor() *Interceptor {
onceInter.Do(func() {
interceptor = &Interceptor{
securityEngine: NewSecurityEngine(), // 보안 엔진 초기화
}
})
return interceptor
}
// NewSecurityEngine: 보안 엔진 초기화 및 규칙 컴파일
func NewSecurityEngine() *securityEngine {
return &securityEngine{
// SQL Injection 패턴 (주요 키워드 및 패턴 감지)
sqlInjectionPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(union\s+select|select\s+.*\s+from|insert\s+into|delete\s+from|drop\s+table|update\s+.*\s+set)`),
regexp.MustCompile(`(?i)(--|\#|\/\*|\*\/|;|'|")`), // 주석 및 인용부호
regexp.MustCompile(`(?i)(\b(OR|AND)\b\s+[\w-]+\s*=\s*[\w-])`), // OR 1=1 같은 패턴
},
// XSS 패턴 (스크립트 태그 및 이벤트 핸들러)
xssPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(<script>|<\/script>|<video|<audio|<img|<iframe|<object|<embed)`),
regexp.MustCompile(`(?i)(javascript:|vbscript:|data:text\/html)`),
regexp.MustCompile(`(?i)(on\w+\s*=)`), // onload=, onerror= 등
},
// Path Traversal 패턴 (상위 디렉토리 접근 시도)
pathTraversal: regexp.MustCompile(`(\.\.(\/|\\)|\.\.$)`),
// Obfuscation 패턴 (비정상적인 문자열, 과도한 특수문자 등)
obfuscationPatterns: []*regexp.Regexp{
regexp.MustCompile(`(?i)(%[0-9a-f]{2}.*%[0-9a-f]{2}.*%[0-9a-f]{2})`), // 반복적인 URL 인코딩
regexp.MustCompile(`[^\x20-\x7E]{5,}`), // 연속된 비출력 문자 (5자 이상)
regexp.MustCompile(`(?i)(base64|eval|exec|system|passthru|shell_exec)`), // 위험한 키워드
regexp.MustCompile(`([!@#$%^&*()_+={}\[\]:;"'<>,.?/\|\\]{5,})`), // 특수문자 연속 5회 이상
},
}
}
func (i *Interceptor) RegisterRoutes(group *gin.RouterGroup) {
group.GET("/intercept", i.handleIntercept)
}
// handleIntercept: 트래픽 인터셉트 및 보안 검사 핸들러
// c: Gin 컨텍스트
func (i *Interceptor) handleIntercept(c *gin.Context) {
// 1. 원본 요청 정보 추출 (Traefik이 채워주는 헤더들)
origMethod := c.GetHeader("X-Forwarded-Method")
origPath := c.GetHeader("X-Forwarded-Uri")
origQuery := c.GetHeader("X-Forwarded-Query") // 쿼리 스트링도 검사 필요
clientIP := c.ClientIP()
userAgent := c.Request.UserAgent()
// 2. 보안 분석 수행
// 빠르고 효율적인 룰 기반 검사
isSecure, reason := i.securityEngine.Analyze(origPath, origQuery, origMethod, userAgent)
if !isSecure {
// 3. 차단: 보안 위협 감지됨
// 로그를 남기는 것이 좋으나 성능상 최소화, 필요시 추가
c.Header("X-Block-Reason", reason)
c.AbortWithStatusJSON(403, gin.H{
"status": "blocked",
"reason": reason,
"ip": clientIP,
})
return
}
// 4. 승인: 안전한 트래픽
c.Status(200)
}
// Analyze: 트래픽 종합 분석
// path: 요청 경로
// query: 쿼리 스트링
// method: HTTP 메서드
// userAgent: 사용자 에이전트
// 반환: 안전 여부(bool), 차단 사유(string)
func (se *securityEngine) Analyze(path, query, method, userAgent string) (bool, string) {
// A. Path Traversal 검사
if se.pathTraversal.MatchString(path) {
return false, "Path Traversal Detected"
}
// 검사 대상 문자열 결합 (Path + Query)
// 대부분의 공격은 URL 파라미터나 경로에 포함됨
fullInput := path
if query != "" {
fullInput += "?" + query
}
// B. SQL Injection 검사
for _, pattern := range se.sqlInjectionPatterns {
if pattern.MatchString(fullInput) {
return false, "SQL Injection Detected"
}
}
// C. XSS 검사
for _, pattern := range se.xssPatterns {
if pattern.MatchString(fullInput) {
return false, "XSS Detected"
}
}
// D. 난독화/이상 문자열 탐지
for _, pattern := range se.obfuscationPatterns {
if pattern.MatchString(fullInput) {
return false, "Obfuscated/Suspicious Payload Detected"
}
}
return true, ""
}
다음은 AI의 도움을 받아서, 딸깍으로 만든 룰 Based 공격 대응 Middleware입니다. 룰 based 검사를 많이 해야한다면, 룰을 batch 처리해서, 검사한다음에 병렬적으로 결과를 받는 방법도 있겠고, 우선은 유저에게 도달시킨다음에 MQ형식으로 유저에게 알림만 주는 식으로 처리할 수도 있겠지만, 저는 일단 이 서비스를 완성하는것이 목표이기 때문에, 간단한 Rule Based 방어로 로직을 구성했습니다.


그 후에는 다음과 같이 metadata.annotations에 다음과 같이 추가하여, middleware를 등록한다.

음 다음과 같이, 꽤 오버헤드가 일어나면 MiddleWare를 통과하는 모습이다. 흠흠.... 스트리밍이나 그런 트래픽은 어떻게 할까.... 또 고민이 든다....
하지만 잘 보면, IP: 10.42.0.1로 고정되어 있는 모습인데, 이게 Kube-Proxy 때문인지 저렇게 바뀐다.
그래서, 다음과 같은 kubectl patch svc traefik -n kube-system -p '{"spec":{"externalTrafficPolicy":"Local"}}' 명령어를 사용하여, Traefix의 외부 IP를 그대로 전송하도록 할 것이다.
바로 적용했고, 다음과 같은 IP가 정상적으로 보이기 시작했다.

아니 그새, 영문도 모르는 IP한마리가 기본 "/"요청을 했다. 저런 악성 봇들이 홈페이지 정보와 프레임 워크 정보를 캐간다음, 악성 스크립트 공격을 하는것이다. 저번에 동아리 보안사고 때문에, 로깅과 보안 대응에 대해 관심이 많아졌다.