본 포스팅은 2편으로 구성되어 있습니다.
네티 채널과 보안
네트워크 데이터 캡쳐
네트워크로 전송된 데이터를 가로채는것을 네트워크 데이터 캡쳐라고 부른다. 방법은 크게 3가지
- tcp dump계열의 네트워크 분석 도구 사용 - 와이어샤크(윈도우), tcpdump(리눅스), 이더리얼
- 브라우저가 제공하는 부가기능이나 플러그인 - 파이어폭스의 파이어버그, 크롬의 개발자도구
- http 및 https와 같이 특정한 프로토콜에 해당하는 데이터만을 수집하는 도구 - 피들러
http는 피들러를 이용하면 데이터를 가로챌 수 있다. tcp도 와이어샤크같은걸 이용해서 가로챌 수 있다.
(다만 테스트해보려면 텔넷서버를 만들고 텔넷을 쏴야하는데, 별도의 운영체제에서 해야한다. 윈도우로만 놓고쏘면 루프백어댑터에대한 패킷을 캡쳐하지못함)
네트워크 보안
네트워크를 보안하게위한 대비책은 크게 2가지이다.
- 전송되는 데이터중에 중요한 부분만 암호화 - 필요한 데이터만 암호화하므로 시스템 리소스를 적게사용하지만 유추한 단점, 복화화 키를 안전하게 공유하는 단점
- 전송되는 모든 데이터를 암호화하는 방법 (채널, 전송계층 암호화, ssl, tls, vpn)
TLS
TLS는 SSL을 기반으로 한 보안 소켓 규격으로서 네트워크로 전송되는 데이터를 암호화하여 보호하는기술. TLS이 표준명칭이지만 SSL는 용어가 더 대중적.
TLS은 비밀키와 공개키방식을 동시에 사용하는데, 핸드쉐이크 방식으로 비밀키를 공개키 알고리즘으로 암호화해서 전송하고 이 공유된 비밀키로 대칭키 암호화알고리즘을 이용하여 서로 통신
네티 서버에 TLS프로토콜을 적용하려면 공개키 암호화에서 사용되는 인증서가 필요. (pem 파일)
SSL 인증서가 필요한데, JDK의 keytool(JKS)같은 인증서 관리도구나 Openssl프로그램을 사용하여 만들 수 있음. (윈도우는 설치해야하고 리눅스는 기본적으로 깔려있음)
- RSA 비밀키생성
// 공개키 알고리즘 RSA로 RSA비밀키를 생성하고 그 비밀키를 AES알고리즘으로 암호화
// privatekey.pem 파일이 RSA비밀키임
openssl genrsa -aes256 -out privatekey.pem 2048
- RSA 비밀키를 이용하여 CSR 생성
// 공개키는 별도로 생성하는게 맞는데, 어차피 인증서 서명요청하기위한 CSR을 생성할때 자동으로 공개키가 포함되어 생성되므로 CSR만 만들면됨.
// CSR파일은 파일을 생성한 기관의 공개키와 회사의 정보가 포함되어있음
openssl req -new -key privatekey.pem -out netty.csr
- 자체 서명
// CSR파일을 상위 인증기관으로 전송하여 인증기관의 비밀키로 전자서명을 받으면 인증서가 완료됨.
// 다만 실제로 서명을 받기위해서는 비용을 지불해야함.. 그래서 테스트를 위해서 인증기관의 비밀키가 아니라 나의 비밀키로 자체서명
openssl x509 -in netty.csr -out netty.crt -req -signkey privatekey.pem -days 365
- 인증서 검증
// 인증서를 발급한 기관의 정보 및 유효기간 확인
openssl x509 -noout -text -in ./netty.crt
// 개인키생성에 사용된 비트수확인
openssl rsa -noout -text -in privatekey.pem
- ASN.1 → PKCS#8 형식으로 변환 (선택)
// privatekey.pem.org로 원본파일을 복사해놓고 이파일을 이용하여 pkcs형식으로 변환
openssl pkcs8 -topk8 -inform PEM -outform PEM -in privatekey.pem.org -out privatekey.pem
네티채널에 보안적용
네티에 SSL/TLS연결을 적용하는 방법은 매우 간단하다.
생성한 인증서와 개인키로 네티의 SslContext인스턴스를 생성하고 채널 파이프라인 가장 앞쪽에 SslContext로부터 생성한 SslHandler를 등록해주면된다.
...
File certChainFile = new File("netty.crt");
File keyFile = new File("privatekey.pem");
// 생성한 인증서와 개인키로 네티의 SslContext인스턴스를 생성
SslContext sslCtx = SslContext.newServerContext(certChainFile, keyFile, "비밀번호");
...
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(...)
.childHandler(new HttpSnoopServerInitializer(**sslCtx**));
...
class HttpSnoopServerInitializer extends ChannelInitializer<SocektChanne>{
private final SslContext sslCtx;
public HttpSnoopServerInitializer (SslContext sslCtx){
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch){
ChannelPipeline p = ch.pipeline();
if (sslCtx != null){
// 채널 파이프라인 가장 앞쪽에 SslContext로부터 생성한 SslHandler를 등록
**p.addLast(sslCtx.newHandler(ch.alloc()));**
}
p.addLast(...)
}
}
https://localhost:443에 접속하면 신뢰할수없는 보안인증서라며 경고창이 뜨게된다. (정상)
이것은 브라우저가 접속한 서버가 전송한 인증서의 전자서명이 신뢰된 인증기관(root CA)가 발행한것이 아니기때문이다 (우리가 자신의 개인키로 서명했음)
@Sharable
private static final TelnetServerHandler SERVER_HANDLER = new TelnetServerHandler();
...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerV3FirstHandler()); // 지금까지는 파이프라인이 초기화될때 new로 새롭게 생성되었다.
p.addLast(SERVER_HANDLER); // 하지만 이런식으로 하나의 데이터 핸들러 객체를 모든 채널파이프라인이 공유하도록 할 수 있음.
// 단 @Sharable 어노테이션으로 공유가능한 코덱이라는것을 나타내야함
}
});
@Sharable // 스레드 safe하게 소스를 짜야함.
public class TelnetServerHandler extends SimpleChannelInboundHandler<String> { ... }
JUnit
네티의 이벤트 핸들러는 네티 프레임워크에서 발생한 이벤트를 처리하는 인터페이스다. 이벤트 핸들러를 테스트하려면 강제로 네티이벤트를 발생시켜야하며 그에따른 이벤트 루프를 설정하는등 굉장히 복잡하다. 네티는 이값은 복잡한 설정없이 이벤트 핸들러를 테스트할 수 있는 EmbeddedChannel클래스를 제공한다. 이 클래스를 사용하면 채널 파이트라인 및 이벤트 루프 설정같은 부가 작업없이 순수하게 이벤트 핸들러를 테스트할 수 있다.
네티를 사용하여 DelimiterBasedFrameDecoder 클래스를 테스트하는 예제이다.
DelimiterBasedFrameDecoder 클래스는 인바운드로 입력된 데이터를 구분자를 기준으로 잘라서 돌려주는 네티의 기본 제공 디코더다.
(디코더 = 인바운드 / 인코더 = 아웃바운드)
public class DelimiterBasedFrameDecoderTest {
@Test
public void testDecoder() {
String writeData = "안녕하세요\\r\\n반갑습니다\\r\\n";
String first = "안녕하세요\\r\\n";
String second = "반갑습니다\\r\\n";
// 최대 8192바이트 데이터를 줄바꿈 문자 기준으로 잘라서 디코딩, 2번재 인수는 디코딩된 데이터에 구분자 포함여부를 뜻함.
DelimiterBasedFrameDecoder decoder = new DelimiterBasedFrameDecoder(8192, false, Delimiters.lineDelimiter());
EmbeddedChannel embeddedChannel = new EmbeddedChannel(decoder); // 테스트할 핸들러 등록
ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes()); // 문자열을 네티의 바이트버퍼로 변환
boolean result = embeddedChannel.writeInbound(request); // 바이트버퍼를 인바운드에 기록 = 클라이언트로부터 데이터를 수신한것과 동일
assertTrue(result);
ByteBuf response = null;
response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다.
assertEquals(first, response.toString(Charset.defaultCharset())); // 안녕하세요/r/n를 돌려줌
response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다.
assertEquals(second, response.toString(Charset.defaultCharset())); // 반갑습니다/r/n를 돌려줌
embeddedChannel.finish(); // 종료
}
}
@Test
public void testEncoder() {
String writeData = "안녕하세요";
ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes()); // 문자열을 네티의 바이트버퍼로 변환
Base64Encoder encoder = new Base64Encoder();
EmbeddedChannel embeddedChannel = new EmbeddedChannel(encoder); // 테스트할 핸들러 등록
boolean result = embeddedChannel.writeOutbound(request); // 바이트버퍼를 아웃바운드에 기록 = 클라이언트로부터 데이터를 수신한것과 동일
assertTrue(result);
ByteBuf response = (ByteBuf) embeddedChannel.readOutbound(); // 아웃바운드 데이터를 읽는다.
String expect = "7JWI64WV7ZWY7IS47JqU";
assertEquals(expect, response.toString(Charset.defaultCharset()));
embeddedChannel.finish(); // 종료
}
@Test
public void testDecoder() {
String writeData = "7JWI64WV7ZWY7IS47JqU";
ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes()); // 문자열을 네티의 바이트버퍼로 변환
Base64Decoder decoder = new Base64Decoder();
EmbeddedChannel embeddedChannel = new EmbeddedChannel(decoder); // 테스트할 핸들러 등록
boolean result = embeddedChannel.writeInbound(request); // 바이트버퍼를 인바운드에 기록
assertTrue(result);
ByteBuf response = (ByteBuf) embeddedChannel.readInbound(); // 인바운드 데이터를 읽는다.
String expect = "안녕하세요";
assertEquals(expect, response.toString(Charset.defaultCharset()));
embeddedChannel.finish(); // 종료
}
'IT > 오픈소스' 카테고리의 다른 글
JPA - 기본 개념 (1) | 2024.01.30 |
---|---|
카프카(Kafka) - 활용정리 (0) | 2023.09.12 |
카프카(Kakfa) 개념정리 (1) | 2023.09.11 |
Docker란? (1) | 2023.03.26 |
네티(Netty) 기본정리 (0) | 2022.02.06 |
댓글