자습서-SQL Injection으로부터 Kubernetes 앱 보호

당신은 베개에서 자전거에 이르기까지 다양한 상품을 판매하는 인기 있는 지역 상점의 IT 부서에서 일하고 있습니다. 그들은 첫 번째 온라인 상점을 시작하려 하지만 출시 전에 보안 전문가에게 사이트가 공개되기 전에 펜 테스트(침투 테스트)를 해달라고 요청했습니다. 안타깝게도 보안 전문가가 문제를 발견했습니다! 온라인 스토어는 SQL Injection에 취약합니다. 보안 전문가는 사이트를 악용하여 데이터베이스에서 사용자 이름과 암호를 비롯한 민감한 정보를 얻을 수 있었습니다.

시간을 절약하기 위해 Kubernetes 엔지니어 팀이 찾아왔습니다. 다행히도 Kubernetes 트래픽 관리 도구를 사용하여 SQL 주입 및 기타 취약점을 완화할 수 있다는 것을 알고 있습니다. 앱을 노출하기 위해 이미 Ingress Controller를 배포했으며 단일 구성에서 이 취약점이 악용되지 않도록 할 수 있습니다. 이제 온라인 상점을 제때에 시작할 수 있습니다.

목차

1. 실습 및 자습서 개요
2. 과제 1: 클러스터 및 취약한 앱 배포
  2-1. Minikube 클러스터 생성
  2-2. 취약한 앱 만들기
3. 과제 2: 앱 해킹
4. 과제 3: NGINX Sidecar 컨테이너를 사용하여 특정 요청 차단
  4-1. NGINX 오픈소스를 Sidecar로 배포
5. 과제 4: 요청을 필터링하도록 NGINX Ingress Controller 구성
  5-1. NGINX Ingress Controller 배포
  5-2. 앱으로 트래픽 라우팅 

  5-3. 필터 테스트
6. 다음 단계

1. 실습 및 자습서 개요

이 모범사례는 NGINX 및 NGINX Ingress Controller를 사용하여 SQL Injection을 차단하는 방법을 보여줍니다. 자신의 환경에서 자습서로 수행하려면 다음이 포함된 머신이 필요합니다.

  • CPU 2개 이상
  • 2GB의 여유 메모리
  • 20GB의 디스크 여유 공간
  • 인터넷 연결
  • Docker, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox 또는 VMware Fusion/Workstation과 같은 컨테이너 또는 가상 머신 관리자
  • minikube 설치
  • Helm 설치

이 모범사례에서는 다음 기술을 사용합니다.

이 모범사례에는 네 가지 과제가 포함되어 있습니다.

  1. 클러스터 및 취약한 앱 배포
  2. 앱 해킹
  3. NGINX Sidecar 컨테이너를 사용하여 특정 요청 차단
  4. 요청을 필터링하도록 NGINX Ingress Controller 구성

2. 과제 1: 클러스터 및 취약한 앱 배포

이 챌린지에서는 minikube 클러스터를 배포하고 Podinfo를 샘플 앱 및 API로 설치합니다.

2-1. Minikube 클러스터 생성

minikube 클러스터를 배포합니다. 몇 초 후 배포가 성공했음을 확인하는 메시지가 표시됩니다.

$ minikube start 

🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default 

2-2. 취약한 앱 만들기

1단계: 배포 만들기
두 개의 마이크로서비스가 포함된 간단한 온라인 스토어 앱을 배포하려고 합니다.

  • MariaDB 데이터베이스
  • 데이터베이스에 연결하여 데이터를 검색하는 PHP 앱

1. 선택한 텍스트 편집기를 사용하여 다음 내용이 포함된 1-app.yaml이라는 YAML 파일을 만듭니다.

apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: app 
spec: 
  selector: 
    matchLabels: 
      app: app 
  template: 
    metadata: 
      labels: 
        app: app 
    spec: 
      containers: 
        - name: app 
          image: f5devcentral/microservicesmarch:1.0.3 
          ports: 
            - containerPort: 80 
          env: 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
            - name: DATABASE_HOSTNAME 
              value: db.default.svc.cluster.local 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: app 
spec: 
  ports: 
    - port: 80 
      targetPort: 80 
      nodePort: 30001 
  selector: 
    app: app 
  type: NodePort 
--- 
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: db 
spec: 
  selector: 
    matchLabels: 
      app: db 
  template: 
    metadata: 
      labels: 
        app: db 
    spec: 
      containers: 
        - name: db 
          image: mariadb:10.3.32-focal 
          ports: 
            - containerPort: 3306 
          env: 
            - name: MYSQL_ROOT_PASSWORD 
              value: root 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 

--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: db 
spec: 
  ports: 
    - port: 3306 
      targetPort: 3306 
  selector: 
    app: db 

2. 앱 및 API를 배포합니다.

$ kubectl apply -f 1-app.yaml 
deployment.apps/app created 
service/app created 
deployment.apps/db created 
service/db created 

STATUS 열에 Running 값으로 표시된 대로 Podinfo Pod가 배포되었는지 확인합니다. 완전히 배포하는 데 30-40초가 걸릴 수 있으므로 다음 단계를 계속하기 전에 모든 Pod가 실행 중인지 확인하기 위해 명령을 다시 실행하는 것이 유용합니다.

$ kubectl get pods  
NAME                  READY   STATUS    RESTARTS   AGE 
app-d65d9b879-b65f2   1/1     Running   0          37s 
db-7bbcdc75c-q2kt5    1/1     Running   0          37s 

브라우저에서 앱을 엽니다.

$ minikube service app 
|-----------|------|-------------|--------------| 
| NAMESPACE | NAME | TARGET PORT |     URL      | 
|-----------|------|-------------|--------------| 
| default   | app  |             | No node port | 
|-----------|------|-------------|--------------| 
😿  service default/app has no node port 
🏃  Starting tunnel for service app. 
|-----------|------|-------------|------------------------| 
| NAMESPACE | NAME | TARGET PORT |          URL           | 
|-----------|------|-------------|------------------------| 
| default   | app  |             | http://127.0.0.1:55446 | 
|-----------|------|-------------|------------------------| 
🎉  Opening service default/app in default browser... 
Protect-Kubernetes-Apps-from-SQL-Injection_product-catalog-home

3. 과제 2: 앱 해킹

샘플 응용 프로그램은 다소 기본적입니다. 여기에는 항목 목록(예: 베개)이 있는 홈페이지와 세부 정보(예: 설명 및 가격)가 있는 일련의 제품 페이지가 포함됩니다. 데이터는 MariaDB 데이터베이스에 저장됩니다. 페이지가 요청될 때마다 데이터베이스에 SQL 쿼리가 실행됩니다.

  • 홈페이지의 경우 페이지는 데이터베이스의 모든 항목을 검색합니다.
  • 상품 페이지의 경우 ID별로 항목을 가져옵니다.

“베개” 제품 페이지를 열면 URL이 /product/1로 끝나는 것을 알 수 있습니다. URL 끝의 “1”은 제품을 식별하는 데 사용되는 ID입니다. 악성 코드가 SQL 쿼리에 직접 삽입되는 것을 방지하려면 요청을 처리하기 전에 사용자 입력을 삭제하는 것이 가장 좋습니다. 그러나 앱이 제대로 구성되지 않고 SQL 쿼리와 데이터베이스에 삽입하기 전에 입력이 이스케이프되지 않으면 어떻게 될까요?

우리는 간단한 실험을 통해 입력이 제대로 이스케이프되었는지 알아낼 것입니다. 데이터베이스에 없는 ID로 ID를 변경하는 것입니다.

1. URL 수정 :

1에서 -1로 끝나는 URL을 수동으로 변경합니다. 문자열이 쿼리에 직접 삽입되기 때문에 제품 ID가 이스케이프되지 않았음을 나타내는 잘못된 제품 ID “-1” 오류 메시지가 반환됩니다.

Protect-Kubernetes-Apps-from-SQL-Injection_xss-injection

데이터베이스 쿼리가 SELECT * FROM some_table WHERE id = “1”과 같다고 가정할 수 있습니다. 이를 악용하기 위해 1을 -1″ — // 다음과 같이 바꿀 수 있습니다.

  • 첫 번째 인용문 “은 첫 번째 쿼리를 완료합니다.
  • quote 뒤에 자체 쿼리를 추가합니다.
  • — // 시퀀스는 나머지 쿼리를 버립니다.

-1″ 또는 1 — //로 끝나는 URL을 변경하려는 경우 쿼리는 다음과 같이 컴파일되어야 합니다.

SELECT * FROM some_table WHERE id = "-1" OR 1 -- //" 
                                      -------------- 
                                      ^  injected  ^ 

데이터베이스에서 모든 행을 선택해야 하므로 해킹에 유용합니다. 이 경우인지 알아보려면 URL 끝을 –1″로 변경하십시오. 결과 오류 메시지는 데이터베이스에 대한 더 유용한 정보를 제공합니다.

Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘”-1″”‘ at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query(‘SELECT * FROM p…’) #1 {main} thrown in /var/www/html/product.php on line 23

이제 -1″ OR 1 ORDER BY id DESC — //를 사용하여 ID별로 데이터베이스 결과를 정렬하려는 시도로 결과 조작을 시작할 수 있습니다. 그러면 데이터베이스의 마지막 항목이 포함된 제품 페이지가 생성됩니다.

데이터베이스가 결과를 주문하도록 하는 것은 흥미롭지만 우리가 좋지 않은 경우에는 특히 유용하지 않습니다. 아마도 우리는 사용자 데이터와 같은 데이터베이스에서 더 많은 정보를 추출할 수 있을 것입니다.

2. 사용자 데이터 추출:

사용자 이름과 암호가 포함된 사용자 테이블이 데이터베이스에 있다고 안전하게 가정할 수 있습니다. 그러나 어떻게 제품 테이블에서 사용자 테이블로 이동할 수 있습니까?

이는 -1″ UNION SELECT * FROM users — //를 사용하여 수행할 수 있습니다.

  • -1″은 첫 번째 쿼리에서 빈 집합을 강제로 반환합니다.
  • UNION은 제품 및 사용자와 같은 두 개의 데이터베이스 테이블을 함께 사용하여 해커가 원래 테이블(제품)과 연결되어서는 안 되는 정보(암호)를 얻을 수 있도록 합니다.
  • UNION SELECT * FROM 사용자는 사용자 테이블의 모든 행을 선택합니다.
  • — // 시퀀스는 이후의 모든 것을 버립니다.

URL이 -1″ UNION SELECT * FROM users — //로 끝나도록 수정하면 새로운 오류 메시지가 나타납니다.

Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query(‘SELECT * FROM p…’) #1 {main} thrown in /var/www/html/product.php on line 23

이 메시지는 제품 테이블과 사용자 테이블이 동일한 수의 열을 가지지 않아 UNION 문을 실행할 수 없음을 알려줍니다. SELECT에 컬럼을 추가함으로써 시행착오를 거쳐 컬럼의 개수를 알 수 있다. users 테이블에 암호 필드가 있을 수 있으므로 다음 순열을 시도할 수 있습니다(각 버전에 열이 추가됨).

-1" UNION SELECT password FROM users; -- // <-- 1 column 
-1" UNION SELECT password,password FROM users; -- // <-- 2 columns 
-1" UNION SELECT password,password,password FROM users; -- // <-- 3 columns 
-1" UNION SELECT password,password,password,password FROM users; -- // <-- 4 columns 
-1" UNION SELECT password,password,password,password,password FROM users; -- // <-- 5 columns 

성공! 5개의 열이 있는 명령문을 사용할 때 작동합니다. 이 응답은 사용자의 비밀번호를 보여줍니다.

Protect-Kubernetes-Apps-from-SQL-Injection_hacked

이제 users 테이블에 총 5개의 열이 있다는 것을 알았으므로 동일한 전술을 사용하여 다른 열 이름을 계속 찾을 수 있습니다. 노출한 비밀번호에 해당하는 사용자 이름을 얻는 것이 유용하지 않을까요? 다음 쿼리는 users 테이블에서 사용자 이름과 암호를 모두 노출합니다. 이 앱이 귀하의 인프라에서 호스팅되지 않는 한 훌륭합니다!

-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //  
Protect-Kubernetes-Apps-from-SQL-Injectionhacked_2

4. 과제 3: NGINX Sidecar 컨테이너를 사용하여 특정 요청 차단

물론 앱을 작성한 사람은 매개변수화된 쿼리 사용과 같은 사용자 입력을 피하기 위해 더 많은 주의를 기울여야 하지만 Kubernetes 엔지니어인 당신도 이 공격이 앱에 도달하는 것을 방지하여 SQL Injection을 피하는 데 도움이 될 수 있습니다. 그렇게 하면 앱이 취약하더라도 공격을 계속 차단할 수 있습니다.

앱을 보호하기 위한 여러 옵션이 있습니다. 이 실습의 나머지 부분에서는 다음 두 가지에 중점을 둘 것입니다.

  1. Pod의 앱에 대한 모든 트래픽을 프록시합니다.
  2. Ingress Controller를 사용하여 클러스터에 들어오는 모든 트래픽을 필터링합니다.

이 과제는 트래픽을 필터링하기 위해 Sidecar 컨테이너를 주입하여 첫 번째 옵션을 구현하는 방법을 탐구합니다. NGINX 오픈소스를 Pod의 Sidecar 컨테이너로 사용하여 모든 트래픽을 프록시하고 URL에 UNION 문이 있는 모든 요청을 거부합니다.

Note: 이 모범사례에서는 이 기술을 설명 목적으로만 활용합니다. 실제로 수동으로 프록시를 Sidecar로 배포하는 것은 최상의 솔루션이 아닙니다(나중에 자세히 설명).

4-1. NGINX 오픈소스를 Sidecar로 배포

1. 아래 내용으로 2-app-sidecar.yaml이라는 YAML 파일을 만들고 다음과 같은 주목할만한 구성 요소를 확인하십시오.

  • NGINX를 실행하는 Sidecar 컨테이너는 포트 8080에서 시작됩니다.
  • NGINX 프로세스는 모든 트래픽을 앱으로 전달합니다.
  • SELECT 또는 UNION 키워드를 포함하는 모든 요청은 거부됩니다.
  • 앱용 서비스는 모든 트래픽을 먼저 NGINX 컨테이너로 라우팅합니다.
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: app 
spec: 
  selector: 
    matchLabels: 
      app: app 
  template: 
    metadata: 
      labels: 
        app: app 
    spec: 
      containers: 
        - name: app 
          image: f5devcentral/microservicesmarch:1.0.3 
          ports: 
            - containerPort: 80 
          env: 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
            - name: DATABASE_HOSTNAME 
              value: db.default.svc.cluster.local 
        - name: proxy # <-- sidecar 
          image: "nginx" 
          ports: 
            - containerPort: 8080 
          volumeMounts: 
            - mountPath: /etc/nginx 
              name: nginx-config 
      volumes: 
        - name: nginx-config 
          configMap: 
            name: sidecar 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: app 
spec: 
  ports: 
    - port: 80 
      targetPort: 8080 # <-- the traffic is routed to the proxy 
      nodePort: 30001 
  selector: 
    app: app 
  type: NodePort 
--- 
apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: sidecar 
data: 
  nginx.conf: |- 
    events {} 
    http { 
      server { 
        listen 8080 default_server; 
        listen [::]:8080 default_server; 

        location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
            deny all; 
        } 

        location / { 
            proxy_pass http://localhost:80/; 
        } 
      } 
    } 
--- 
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: db 
spec: 
  selector: 
    matchLabels: 
      app: db 
  template: 
    metadata: 
      labels: 
        app: db 
    spec: 
      containers: 
        - name: db 
          image: mariadb:10.3.32-focal 
          ports: 
            - containerPort: 3306 
          env: 
            - name: MYSQL_ROOT_PASSWORD 
              value: root 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 

--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: db 
spec: 
  ports: 
    - port: 3306 
      targetPort: 3306 
  selector: 
    app: db

2. Sidecar 배포

$ kubectl apply -f 2-app-sidecar.yaml 
deployment.apps/app configured 
service/app configured 
configmap/sidecar created 
deployment.apps/db unchanged 
service/db unchanged 

필터 테스트

앱으로 돌아가서 SQL Injection을 다시 시도하여 Sidecar가 트래픽을 필터링하는지 테스트합니다. NGINX는 앱에 도달하기 전에 요청을 차단합니다!

-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
Protect-Kubernetes-Apps-from-SQL-Injection_forbidden

5. 과제 4: 요청을 필터링하도록 NGINX Ingress Controller 구성

마지막 챌린지에서 설명한 방식으로 앱을 보호하는 것은 흥미롭고 교육적인 경험이지만 다음과 같은 이유로 프로덕션에는 권장되지 않습니다.

  • 완전한 보안 솔루션이 아닙니다.
  • 확장할 수 없습니다(이 보호 기능을 여러 앱에 쉽게 적용할 수 없음).
  • 업데이트는 복잡하고 비효율적입니다.

Ingress Controller를 사용하여 동일한 기능을 모든 앱으로 확장하는 것이 훨씬 더 나은 솔루션입니다! Ingress Controller는 웹 애플리케이션 방화벽(WAF)에서 인증 및 권한 부여에 이르기까지 모든 종류의 보안 기능을 중앙 집중화하는 데 사용할 수 있습니다.

shared_ingress

5-1. NGINX Ingress Controller 배포

NGINX Ingress Controller를 설치하는 가장 빠른 방법은 Helm을 사용하는 것입니다.

1. Helm에 NGINX 저장소를 추가합니다. 

$ helm repo add nginx-stable https://helm.nginx.com/stable  

2. F5 NGINX에서 유지 관리하는 NGINX 오픈소스 기반 NGINX Ingress Controller를 다운로드하여 설치합니다. 이 명령에는 enableSnippets=true가 포함되어 있습니다. 스니펫은 SQL Injection을 차단하도록 NGINX를 구성하는 데 사용됩니다. 출력의 마지막 줄은 성공적인 설치를 확인합니다.

$ helm install main nginx-stable/nginx-ingress \ 
--set controller.watchIngressWithoutClass=true  
--set controller.service.type=NodePort \ 
--set controller.service.httpPort.nodePort=30005 \ 
--set controller.enableSnippets=true 
NAME: main  
LAST DEPLOYED: Tue Feb 22 19:49:17 2022  
NAMESPACE: default  
STATUS: deployed  
REVISION: 1  
TEST SUITE: None  
NOTES: The NGINX Ingress Controller has been installed.  

3. STATUS 열에 Running 값으로 표시된 대로 NGINX Ingress Controller Pod가 배포되었는지 확인합니다.

$ kubectl get pods   
NAME                                  READY   STATUS    RESTARTS   AGE  
main-nginx-ingress-779b74bb8b-mtdkr   1/1     Running   0          18s  

5-2. 앱으로 트래픽 라우팅 

1. 다음 내용으로 3-ingress.yaml이라는 YAML 파일을 만듭니다. 트래픽을 앱으로 라우팅하는 데 필요한 Ingress 매니페스트를 정의합니다(이번에는 트래픽이 사이드카 프록시를 통과하지 않음). NGINX Ingress Controller는 주석으로 정의된 스니펫으로 사용자 정의되며, 스니펫에는 마지막 챌린지와 같이 사이드카 컨테이너에 삽입된 동일한 라인이 포함되어 있습니다.

apiVersion: v1 
kind: Service 
metadata: 
  name: app-without-sidecar 
spec: 
  ports: 
    - port: 80 
      targetPort: 80 
  selector: 
    app: app 
--- 
apiVersion: networking.k8s.io/v1 
kind: Ingress 
metadata: 
  name: entry 
  annotations: 
    nginx.org/server-snippets: | 
      location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
          deny all; 
      } 
spec: 
  ingressClassName: nginx 
  rules: 
    - host: "example.com" 
      http: 
        paths: 
          - backend: 
              service: 
                name: app-without-sidecar 
                port: 
                  number: 80 
            path: / 
            pathType: Prefix 

2. Ingress 리소스를 배포합니다.

$ kubectl apply -f 3-ingress.yaml  
service/app-without-sidecar created 
ingress.networking.k8s.io/entry created 

5-3. 필터 테스트

Ingress Controller가 수신하는 Pod로 트래픽을 라우팅하는 새 URL이 필요합니다. URL을 얻으려면 올바른 호스트 이름을 사용하여 NGINX Ingress 파드(Pod)에 요청을 발행하는 임시 busybox 컨테이너를 시작하십시오.

$ kubectl run -ti --rm=true busybox --image=busybox 
$ wget --header="Host: example.com" -qO- main-nginx-ingress 
<!DOCTYPE html> 
<html lang="en"> 

<head> 
# truncated output 

이제 SQL Injection을 실행해 보십시오. 이번에도 NGINX가 공격을 차단합니다!

$ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 
0id=1%20--%20//' 
wget: server returned error: HTTP/1.1 403 Forbidden 

6. 다음 단계

Kubernetes는 기본적으로 안전하지 않습니다. Ingress Controller를 사용하면 SQL(및 기타 많은) 취약점을 완화할 수 있습니다. 그러나 명심하십시오. WAF와 유사한 기능을 방금 구현했지만 Ingress Controller는 WAF(웹 애플리케이션 방화벽)를 대체하지 않으며 앱을 안전하게 설계하는 것도 대체하지 않습니다. 능숙한 해커는 코드를 약간만 변경해도 UNION 해킹이 작동하도록 할 수 있습니다. Ingress Controller는 여전히 대부분의 보안을 중앙 집중화하는 강력한 도구로서 중앙 집중식 인증 및 권한 부여 사용 사례(mTLS, 싱글 사인온)와 NGINX App Protect WAF와 같은 강력한 WAF를 포함하여 효율성과 보안을 향상시킵니다.

앱과 아키텍처의 복잡성으로 인해 더 세밀한 제어가 필요할 수 있습니다. 조직에 Zero Trust가 필요하고 종단 간 암호화가 필요한 경우 Service Mesh를 고려하십시오. 서비스 간 통신(east-west 트래픽)이 있는 경우 Service Mesh를 사용하면 해당 수준에서 트래픽을 제어할 수 있습니다.