MQTT 트래픽 암호화 및 인증: IoT용 NGINX Plus

NGINX Plus 및 MQTT 사물 인터넷(IoT)에 대한 이 2부작 포스트 시리즈의 첫 번째 부분에서는 NGINX Plus(TCP 및 UDP 애플리케이션을 지원하는 완전한 기능을 갖춘 애플리케이션 제공 컨트롤러(ADC))가 어떻게 증가하는지 보여주었습니다. IoT 애플리케이션의 가용성과 신뢰성. 이 두 번째 포스트에서는 NGINX Plus를 사용하여 IoT 보안을 개선하는 두 가지 방법에 대해 설명합니다. 두 포스트는 다음 사용 사례를 다룹니다.

목차

1. MQTT 서버에서 TLS Termination 오프로드
2. 클라이언트 인증서를 사용하여 MQTT 클라이언트 인증
3. MQTT 클라이언트 인증을 위한 NGINX Plus 구성
4. MQTT 클라이언트 인증을 위한 JavaScript 코드
5. MQTT 클라이언트 인증 테스트
6. 결론

1. MQTT 서버에서 TLS Termination 오프로드

첫 번째 포스트의 예제 사용 사례에서 모든 MQTT 트래픽은 일반 텍스트이며 암호화되지 않았습니다. IoT 보안을 개선하려면 IoT 디바이스 또는 IoT 게이트웨이에서 TLS 암호화를 지원할 때마다 TLS를 사용하여 클라이언트와 upstream 서버 간에 전달되는 MQTT 데이터를 암호화하는 것이 가장 좋습니다. 암호화는 공용 네트워크를 통과할 때 이동 중인 데이터를 보호하는 효과적인 방법이지만 수백만 대의 디바이스가 있는 프로덕션 환경에서는 MQTT서버에 막대한 부하를 줄 수 있습니다.

사진 1과 같이 NGINX Plus는 MQTT서버에서 TLS 암호화와 관련된 CPU 집약적 워크로드를 오프로드할 수 있습니다(일반적으로 SSL 오프로딩이라고 함). 이러한 관심사 분리를 통해 로드 밸런싱 계층과 MQTT 데이터 처리 계층이 독립적으로 확장될 수 있으며 MQTT 테스트 환경에 대한 간단한 수정만 필요합니다.

(첫 번째 포스트에서와 같이 Mosquitto command line tool를 클라이언트로 사용하고 Docker 컨테이너 내에서 MQTT 브로커로 실행되는 HiveMQ 인스턴스를 사용합니다. 설치 지침은 첫 번째 포스트의 테스트 환경 만들기를 참조하세요.)

TLS 암호화로 IoT 보안을 개선하기 위해 NGINX Plus는 MQTT 장치에 대한 TLS 종료(SSL 오프로딩이라고도 함)를 수행합니다.

사진 1. NGINX Plus는 MQTT 클라이언트에 대한 TLS Termination를 수행합니다.

NGINX Plus 인스턴스에 TLS 인증서 키 쌍을 제공한 후 Stream SSL 모듈의 지시문을 사용하여 TLS Termination를 활성화할 수 있습니다.


server {
    listen 8883 ssl; # MQTT secure port
    preread_buffer_size 1k;
    js_preread getClientId;

    ssl_certificate     /etc/nginx/certs/my_cert.crt;
    ssl_certificate_key /etc/nginx/certs/my_cert.key;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:128m; # 128MB ~= 500k sessions
    ssl_session_tickets on;
    ssl_session_timeout 8h;

    proxy_pass hive_mq;
    proxy_connect_timeout 1s;

    access_log /var/log/nginx/mqtt_access.log mqtt;
    error_log  /var/log/nginx/mqtt_error.log info; # nginScript debug logging
}

server 블록은 이전 포스트의 세션 지속성에 대한 구성과 동일합니다. 단, 2행은 보안 MQTT 트래픽을 위한 표준 포트 번호인 8883을 지정하고 6~11행은 클라이언트에서 TLS 연결을 종료하도록 서버를 구성하기 위해 추가됩니다. 인증서를 얻고 설치하는 방법을 설명하는 것은 이 포스트의 범위를 벗어납니다. 프로덕션 IoT 배포는 일반적으로 자체 PKI(공개 키 인프라)를 사용합니다. 또한 TLS 관련 지시문을 검토하지 않습니다. TLS Termination에 대한 자세한 내용은 NGINX Plus 관리자 가이드를 참조하세요.

이 구성을 사용하면 Mosquitto MQTT클라이언트를 사용하여 암호화된 메시지를 게시할 수 있습니다. 보안 MQTT 포트(8883)와 NGINX 인스턴스(cafile.pem)에서 서버 인증서를 발급한 인증 기관의 공개 키가 포함된 파일을 지정합니다.

$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 8883 --cafile cafile.pem
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Feb/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18832 thing001

2. 클라이언트 인증서를 사용하여 MQTT 클라이언트 인증

[메모 – 다음 사용 사례는 NGINX JavaScript 모듈의 많은 사용 사례 중 하나에 불과합니다. 전체 목록은 NGINX JavaScript 모듈의 사용 사례를 참조하십시오.

NGINX JavaScript 0.2.4에서 도입된 stream 모듈용 리팩터링된 세션(들) 객체를 사용하도록 포스트이 업데이트되었습니다.]

IoT 사용 사례를 위한 MQTT의 성공과 광범위한 채택에도 불구하고 프로토콜 자체는 클라이언트의 ID를 확인하기 위한 제공이 매우 제한적입니다. 인증은 MQTT CONNECT 패킷의 사용자 이름 및 암호 필드를 사용하여 지원되지만 실제로는 관리하기 어렵습니다.

MQTT 사양에서 공식적으로 지원하지는 않지만 X.509 클라이언트 인증서는 일반적으로 클라이언트를 인증하는 데 사용되며 상호 인증이 발생할 수 있도록 TLS 암호화와 결합할 때 특히 유용합니다.

  • 클라이언트는 서버의 신원을 확인합니다.
  • 서버는 클라이언트의 신원을 확인합니다.

NGINX Plus는 MQTT클라이언트가 인증서를 제공해야 하고 인증서의 일반 이름(CN)이 MQTT ClientId와 일치하도록 TLS Termination을 클라이언트 인증서 인증과 결합할 수 있습니다. ClientId를 X.509 인증서의 CN에 연결함으로써 MQTT서버는 수신된 메시지가 신뢰할 수 있는 정품 디바이스에서 온 것인지 확인할 수 있습니다.

MQTT 클라이언트를 인증하여 IoT 보안을 개선하기 위해 NGINX Plus는 X.509 클라이언트 인증서(여기서는 사실상 TLS 인증서임) 및 개인 키를 처리합니다.

사진 2. 상호인증을 위한 X.509 인증서 및 Private Key 제공

3. MQTT 클라이언트 인증을 위한 NGINX Plus 구성

이 사용 사례의 경우 이전 섹션의 NGINX Plus 구성(클라이언트 인증서 인증 활성화)과 이전 포스트의 NGINX JavaScript 코드(인증서 CN을 ClientId와 일치시키기 위해)를 모두 확장합니다. 다음 구성 스니펫을 server 블록에 추가하면 클라이언트 인증서 인증이 활성화됩니다.


    ssl_verify_client on; # Clients must supply certificate
    ssl_verify_depth 2;   # In case of intermediate CA
    ssl_client_certificate /etc/nginx/certs/cafile.pem; # Issuer of client certificates

ssl_verify_client on 지시문은 클라이언트가 인증서를 제공해야 함을 NGINX에 알립니다. 이 예에서 클라이언트 인증서는 중간 인증 기관에서 발급하므로 ssl_verify_depth 지시문을 사용하여 NGINX에 두 가지 수준의 발급자 인증서가 있음을 알립니다. ssl_client_certificate 지시문은 클라이언트에 인증서를 발급하는 인증 기관(CA)에 대한 공용 인증서의 디스크 위치를 지정합니다. NGINX는 클라이언트 인증 프로세스의 일부로 공개 CA 인증서를 사용합니다.

4. MQTT 클라이언트 인증을 위한 JavaScript 코드

마지막으로 이전 포스트에서 설명한 세션 지속성 사용 사례를 위해 생성한 NGINX JavaScript 코드(mqtt.js)를 확장합니다. 추가 코드는 CONNECT 패킷에 표시된 MQTT ClientId가 동일한 클라이언트에 발급된 인증서의 CN과 동일한 값을 가지고 있는지 확인합니다.

function parseCSKVpairs(cskvpairs, key) {
    if ( cskvpairs.length ) {
        var kvpairs = cskvpairs.split(',');
        for ( var i = 0; i < kvpairs.length; i++ ) {
            var kvpair = kvpairs[i].split('=');
            if ( kvpair[0].toUpperCase() == key ) {
                return kvpair[1];
            }
        }
    }
    return ""; // Default condition
}

X.509 인증서에서 CN 값을 추출하기 위해 parseCSKVpairs 함수를 추가합니다. 이것은 다른 함수(getClientId 함수)에 의해 호출되므로 파일에서 그 위에 나타나야 합니다.

var client_messages = 1;
var client_id_str = "-";

function getClientId(s) {
   s.on('upload', function (data, flags) {
        if ( data.length == 0  ) {  // Initial calls may contain no data, so
            s.log("No buffer yet"); // ask that we get called again
            //s.done(1);            // (supposing that code=1 means that)
            return;
        } else if ( client_messages == 1 ) { // Connect is first packet from the client
            // Connect packet is 1, using upper 4 bits (00010000 to 00011111)
            var packet_type_flags_byte = data.charCodeAt(0);
            s.log("MQTT packet type+flags = " + packet_type_flags_byte.toString());
            if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) {
                // Calculate remaining length with variable encoding scheme
                var multiplier = 1;
                var remaining_len_val = 0;
                var remaining_len_byte;
                for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) {
                    remaining_len_byte = data.charCodeAt(remaining_len_pos);
                    if ( remaining_len_byte == 0 ) break; // Stop decoding on 0
                    remaining_len_val += (remaining_len_byte & 127) * multiplier;
                    multiplier *= 128;
                }

                // Extract ClientId based on length defined by 2-byte encoding
                var payload_offset = remaining_len_pos + 12; // Skip fixed header
                var client_id_len_msb = data.charCodeAt(payload_offset).toString(16);
                var client_id_len_lsb = data.charCodeAt(payload_offset + 1).toString(16);
                if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb;
                var client_id_len_int = parseInt(client_id_len_msb + client_id_len_lsb, 16);
                client_id_str = data.substr(payload_offset + 2, client_id_len_int);
                s.log("ClientId value  = " + client_id_str);

14-45행의 변수 선언 및 getClientId 함수는 세션 지속성 사용 사례를 위해 생성한 mqtt.js 파일의 1-32행과 동일합니다.


                // If client authentication then check certificate CN matches ClientId
                var client_cert_cn = parseCSKVpairs(s.variables.ssl_client_s_dn, "CN");
                if ( client_cert_cn.length && client_cert_cn != client_id_str ) {
                    s.log("Client certificate common name (" + client_cert_cn + ") does not match client ID");
                    s.deny(); // Close the TCP connection (logged as 500)
                }
            } else {
                s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString());
            }
        }
        client_messages++;
        s.allow();
    });
}

function setClientId(s) {
    return client_id_str;
}

48~49행은 ClientId를 인증서 CN과 일치시키는 것과 관련이 있습니다. CN 자체는 인증서의 주체 고유 이름에 포함되며 그 값은 $ssl_client_s_dn 변수에서 사용할 수 있습니다. NGINX JavaScript는 s.variables 개체를 통해 모든 NGINX 변수에 액세스할 수 있습니다. 이 변수에는 쉼표로 구분된 Key-Value 쌍 목록으로 수많은 속성이 포함되어 있습니다.

마지막 행(53–64)은 세션 지속성 사용 사례에 대한 mqtt.js 파일의 34–45행과 동일합니다.

5. MQTT클라이언트 인증 테스트

이 구성을 사용하면 Mosquitto 클라이언트를 사용하여 인증되고 암호화된 메시지를 테스트 환경에 보낼 수 있습니다. 이 openssl x509(1) 명령을 실행하여 thing001에 발급된 인증서 키 쌍을 검사할 수 있습니다.

$ openssl x509 -subject -noout < thing0001.crt
subject= /C=GB/L=Cambridge/O=example.com/OU=Example CA/CN=thing001

이제 이 인증서 키 쌍을 테스트 환경에 제공할 수 있습니다.

$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 8883 --cafile cafile.pem --cert thing0001.crt --key thing0001.key
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [24/Feb/2017:14:37:08 +0000] TCP 200 23 4 127.0.0.1:18832 thing001

연결을 설정하려는 클라이언트가 인증서와 일치하지 않는 ClientId를 제공하거나 인증서 제공에 전혀 실패하는 경우 NGINX Plus는 인증되지 않은 메시지가 upstream MQTT서버에 도달하지 않도록 즉시 연결을 종료합니다. 따라서 NGINX Plus는 악의적이거나 잘못된 클라이언트로부터 MQTT서버를 추가로 보호합니다.

$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "BADTHING" -p 8883 --cafile cafile.pem --cert thing0001.crt --key thing0001.key
Client BADTHING sending CONNECT
Error: The connection was lost.
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "NOCERT" -p 8883 --cafile cafile.pem
Client NOCERT sending CONNECT
Error: The connection was lost.
$ tail --lines=2 /var/log/nginx/mqtt_access.log
192.168.91.1 [24/Feb/2017:14:37:16 +0000] TCP 500 0 0 - BADTHING
192.168.91.1 [24/Feb/2017:14:42:16 +0000] TCP 500 0 0 - -

6. 결론

NGINX Plus를 사용하여 MQTT 서버에서 암호화 및 인증 워크로드를 모두 오프로드하면 IoT 보안은 물론 IoT 배포의 전체 성능 및 트래픽 용량이 향상됩니다.

NGINX Plus와 함께 NGINX JavaScript를 사용해 보려면 지금 무료 30일 평가판을 시작하거나 당사에 연락하여 IoT 프로젝트에 대해 이야기하십시오.

사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.

NGINX STORE를 통한 솔루션 도입 및 기술지원 무료 상담 신청

* indicates required