NGINX Unit + Spring Boot 제로 트러스트 구현

NGINX Unit 두 번째 포스트입니다. 첫 번째 포스트에서는 NGINX Unit 앱 서버에 대해 소개와 주요 특징에 대해 설명하였습니다. 오늘 포스트는 Spring Boot 애플리케이션에서 NGINX Unit을 사용하여 제로 트러스트 환경을 구현하는 것에 대해 설명합니다.

애플리케이션을 보호하는 것은 결코 쉬운 일이 아니지만 클라우드 환경에서 애플리케이션을 실행하는 것은 훨씬 더 많은 과제를 안겨줍니다. 그에 따른 좋은 솔루션으로 보이는 한 가지 개념은 가트너(Gartner)에서 다음과 같이 정의한 “제로 트러스트(Zero trust security model)“입니다.

그러나 클라우드 환경 컨텍스트에서 “제로 트러스트”는 정확히 어떻게 작동하며 이를 구현하는 데 사용할 수 있는 기술은 무엇이 있을까요? 이 블로그에서는 일반적인 사용 사례의 맥락에서 제로 트러스트를 고려할 것입니다.

예를 들어 당신은 Java로 구동되는 다양한 API 및 서비스 세트를 보유한 보험 회사입니다. 이제 클라우드로 마이그레이션(Migration)했으므로 프로덕션 워크로드는 CI/CD 파이프라인에 의해 자동으로 구축되고 퍼블릭 클라우드 제공자(CSP)의 Kubernetes 클러스터에 배포됩니다. 민감한 고객 정보를 다룰 때 한 가지 주요 요구 사항은 TLS로 모든 트래픽을 암호화하는 것입니다.

Edge Load Balancer와 Ingress Controller에서 암호화를 활성화했지만 Ingress Controller와 애플리케이션 자체 간의 트래픽을 암호화하는 가장 좋은 방법은 무엇일까요? 여기에는 애플리케이션 서버가 TLS를 처리할 수 있도록 하는 작업이 포함됩니다.

많은 Java 개발 환경에서 Apache Tomcat을 애플리케이션 서버로 사용하고 Spring Boot를 프레임워크로 사용하여 Java 자체보다 더 쉽게 프로덕션 Spring 애플리케이션을 빌드합니다. 이 블로그에서는 HTTPS 트래픽을 처리할 수 있는 애플리케이션에 대해 Apache Tomcat(및 NGINX Unit)으로 Spring Boot를 구성하는 방법을 자세히 보여줄 것입니다.

목차

1. Spring Boot: 애플리케이션 HTTPS(TLS) 암호화 이야기
2. NGINX Unit 앱 서버로 구성해봅니다.
3. NGINX Unit HTTPS 활성화(Enabling)
4. 요약
5. NGINX Unit 시리즈

1. Spring Boot: 애플리케이션 HTTPS(TLS) 암호화 이야기

GitHub에 60,000개의 별이 있는 Spring Boot는 Java 프레임워크 진영에서 스타 같은 존재입니다. 시작하기 쉽고 가벼우며 동시에 강력합니다. Spring Boot 프로젝트는 Apache Tomcat과 같은 내장 애플리케이션 서버(WAS)를 사용하여 자체 포함된 .jar 파일로 컴파일할 수 있습니다. Java 서비스를 시작하려면 .jar 파일을 실행하고 노출된 포트(기본값: 8080)로 요청(Request)을 시작하기만 하면 됩니다.

다음은 서비스가 TLS 연결(HTTPS 트래픽)을 올바르게 처리하기 위해 수행해야 하는 몇 가지 추가 단계입니다. 단계에 대한 자세한 내용은 Spring 설명서를 참조하세요.

이 가이드는 자체 서명된 인증서(certificate) 및 키(key)에 대한 것이지만 프로덕션 환경의 경우 공식 인증 기관(CA)의 인증서 키 쌍(certificate‑key pair)을 대체하는 것이 좋습니다.

1. 인증서(certificate)와 키(key)가 포함된 키 저장소(key store)를 만듭니다.

# keytool -genkey -alias tomcat -keyalg RSA -keystore certstore

2. Tomcat이 액세스할 수 있어야 하는 컨테이너 이미지의 키 저장소(key store)를 끈으로 묶습니다.

3. 다음 속성을 application.properties 파일에 추가하고 secret을 적절한 비밀번호로 바꿉니다.

server.port = 8443
server.ssl.key-store = classpath:keystore.jks
server.ssl.key-store-password = secret

이 구성을 사용하면 Spring Boot 애플리케이션이 HTTPS 연결을 위해 포트 8443에서 수신 대기(listening)합니다. 하지만 HTTP 연결도 수락하려면 어떻게 해야 할까요? application.properties 파일에서 HTTPS를 구성한 후에는 HTTP도 구성할 수 없습니다. Java 코드 자체에서 HTTP 처리를 구현해야 합니다. 좋은 예는 GitHub의 spring-projects 리포지토리를 참조하세요.

따라서 Spring Boot와 같은 애플리케이션 프레임워크에 Layer 4 TLS 암호화를 위임하는 것이 가능하지만 매우 간단하지는 않습니다. 다른 언어 및 프레임워크(예: Ruby 및 Rails, Python 및 Flask)로 애플리케이션을 작성하는 경우 상황이 훨씬 더 복잡해집니다. 각 프레임워크에는 리스너(listener)를 구성하고 키(key) 및 인증서(certificate)를 처리하는 고유한 방법이 있습니다. 다행히 상황을 훨씬 간단하게 만드는 것이 있습니다!

2. NGINX Unit 앱 서버로 구성해봅니다.

NGINX Unit은 Unix 계열 시스템을 위해 핵심(Core) NGINX 개발팀이 작성한 오픈소스 다중 언어(Multiple-languages) 애플리케이션(App) 서버이며, 리버스 프록시 및 정적 파일 서버(웹 서버) 기능도 포함합니다. NGINX Unit을 사용하면 표준화된 API를 사용하여 다양한 언어로 작성된 애플리케이션을 동시에 실행하고 관리할 수 있습니다. 현재로서는 Java 외에도 어셈블리, Go, JavaScript(Node.js®), Perl, PHP, Python, Ruby.

또한 NGINX Unit을 사용하여 HTTP 및 HTTPS 인터페이스를 사용하는 애플리케이션과 독립적으로 구성할 수 있습니다.

Spring Boot API 예제를 통해 이 강력한 기능을 살펴보겠습니다. 먼저 NGINX Unit 서버용 Spring Boot 애플리케이션을 빌드해야 합니다. Unit이 Java Servlet API 버전 3을 구현함에 따라 유일한 변경 사항은 Gradle 또는 Maven 빌드 정의에 추가된 행뿐입니다. 테스트에 Gradle을 사용했습니다.

1. build.gradle 파일에 war 플러그인(plug-in)을 추가합니다.

plugins {
  id 'org.springframework.boot' version '2.4.4'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
  id 'war'
}

2. .war 파일을 빌드합니다.

# ./gradlew build

결과 파일은 build/libs/rootProject‑Version.war입니다. 여기서:

  • rootProject는 settings.gradle 파일에 정의되어 있습니다.
  • 버전은 build.gradle 파일에 정의되어 있습니다.

3. config.json이라는 파일에서 NGINX Unit 구성(Configuration)을 정의합니다.

{
    "listeners": {
        "*:8080": {
            "pass": "applications/java"
        }
    },
    "applications": {
        "java": {
            "user": "unit",
            "group": "unit",
            "type": "java",
            "environment": {
                "Deployment": "0.0.1"
            },
            "classpath": [],
            "webapp": "/path/to/build/libs/demo-0.0.1-SNAPSHOT.war"
        }
    }
}

4. 구성(Configuration)을 활성화(Activate)합니다(자세한 내용은 설명서 참조).

# curl -X PUT --data-binary @config.json --unix-socket \
       /path/to/control.unit.sock http://localhost/config/applications/java-app

이게 다입니다. 이제 Spring Boot 애플리케이션은 NGINX Unit에서 실행 중입니다. Apache Tomcat 또는 기타 Java 웹 애플리케이션 서버(WAS)가 필요하지 않습니다.

3. NGINX Unit HTTPS 활성화(Enabling)

다음 단계를 따르면 쉽습니다. (위와 같이 자체 서명 인증서를 사용하고 있습니다. 프로덕션 환경에서는 CA 서명 인증서를 사용해야 합니다.)

1. 자체 서명된 인증서 번들(certificate bundle)을 생성합니다.

# cat cert.pem ca.pem key.pem > bundle.pem

2. 번들(bundle)을 NGINX Unit에 업로드:

# curl -X PUT --data-binary @bundle.pem --unix-socket \
       /path/to/control.unit.sock http://localhost/certificates/bundle

3. listener.json이라는 파일에서 HTTPS 리스너(listener)에 대한 구성(configuration)을 정의합니다.

"127.0.0.1:443": {
    "pass": "applications/java-app",
    "tls": {
        "certificate": "bundle"
    }
}

4. 새 리스너(new listener)를 활성화(Activate)합니다.

# curl -X PUT --data-binary @listener.json --unix-socket \ 
/path/to/control.unit.sock http://localhost/config/listeners

이제 애플리케이션이나 NGINX Unit를 다시 시작하거나 재부팅하지 않고도 애플리케이션에서 TLS 암호화 연결을 허용합니다. 그러나 가장 강력한 것은 이전 프로세스가 NGINX Unit에서 지원하는 모든 프로그래밍 언어 및 프레임워크로 작성된 애플리케이션에 대해 동일하다는 것입니다. HTTPS를 구성하기 위해 프로그래밍 언어별 세부 정보를 조사할 필요가 없습니다.

4. 요약

강력한 NGINX Unit listener 기능은 암호화가 애플리케이션이 아닌 listener 수준에서 적용되기 때문에 HTTP 및 HTTPS 지원을 간단하고 완전히 애플리케이션에 구애받지 않게 합니다. SNI(서버 이름 표시) 및 사용자 지정 OpenSSL 구성 명령과 같은 다른 TLS 기능에 대해 알아보려면 NGINX Unit 문서를 참조하십시오.

NGINX Unit을 시작하려면 설치 지침을 참조하십시오.

5. NGINX Unit 시리즈