Android에서 평문 차단이라고 불리는 것은 전혀 다른 두 가지입니다. 하나는 앱 내부의 정책으로, 자체 네트워크 스택에 일반 HTTP를 거부하라고 지시합니다. 다른 하나는 기기 수준의 구성으로, 보통 트래픽을 VPN으로 보내 기기 전체를 한 번에 보호하려 합니다. 둘은 다른 문제를 풀며, 후자로 전자를 고칠 수는 없습니다. 터널은 기기와 출구 사이 구간을 보호하지만, HTTP를 쓰는 앱은 그 출구 너머에 대해서도 여전히 HTTP를 씁니다.

하나의 이름 아래 두 가지 문제

첫 번째 문제는 페이로드입니다. 앱이 자격 증명이나 토큰을 종단 간 암호화되지 않은 요청에 기록하므로, 그 요청을 볼 수 있는 모든 구간이 읽을 수 있습니다. 두 번째는 경로입니다. 앱이 완전히 올바르더라도, 어디로 연결하는지를 로컬 Wi-Fi 사업자에게 알리고 싶지 않을 수 있습니다. VPN이 다루는 것은 두 번째뿐입니다.

이 구분이 수정의 주인을 정합니다. 페이로드는 앱을 배포하는 쪽의 몫입니다. 어떤 URL을 호출하고 인증서를 검증하는지를 그 코드만 결정하기 때문입니다. 경로는 기기나 네트워크의 몫입니다. 패킷이 어디로 가는지의 문제이지 무엇을 담는지의 문제가 아니기 때문입니다.

경로 하나: 직접 관리하는 앱

앱을 배포한다면 수정은 전송 방식의 결정과 이를 강제하는 정책입니다. 모든 엔드포인트를 HTTPS로 돌리고, 호스트 이름과 인증서 검증을 켜 두고, 일반 HTTP 폴백은 설정 플래그 하나 거리에 두지 말고 삭제하십시오. 리디렉션은 여기서 도움이 되지 않습니다. 301을 받는 HTTP 요청은 이미 헤더와 본문을 평문으로 보냈으므로, 리디렉션이 보호해 준다는 가정으로 자격 증명을 보내면 안 됩니다.

Android 7.0 이후에서는 Network Security Configuration이 정책을 표현합니다. 아래 파일은 가장 작은 실용 버전으로, 도메인별 예외가 없다면 어디서나 평문을 거부합니다.

<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
</network-security-config>

매니페스트에서 그 리소스를 참조하고, 배포 전에 이미 존재하는 예외를 읽으십시오. 오래된 디버그 도메인이나 결제 사업자 SDK 항목이 보호하려던 호스트의 평문을 조용히 다시 허용할 수 있습니다.

버전에 관한 두 가지를 알아 둘 만합니다. API 28 이상을 대상으로 하는 앱은 이미 기본적으로 평문을 거부하므로, 여전히 일반 HTTP를 쓰는 앱은 오래된 API 수준을 대상으로 하거나 어딘가에 예외가 있습니다. 또한 네트워크 보안 구성이 있으면 오래된 android:usesCleartextTraffic 매니페스트 속성은 더 이상 판단하지 않으며, 그래서 그 플래그를 바꿔도 아무 일이 없는 것처럼 보이기도 합니다.

경로 둘: 관리할 수 없는 앱

설치된 타사 앱은 사용자 설정으로 보안 구성을 다시 쓸 수 없습니다. 현실적인 선택은 제품 자체의 보안 연결 제어, 이미 TLS를 쓰는 유지 관리되는 대안, 기기가 실행할 수 있는 것을 제한하는 관리 기기 정책입니다. 서로 다른 약속이며, 기기 집단으로 확장되는 것은 세 번째뿐입니다.

네트워크 규칙은 보기보다 약한 대체물입니다. 포트 443만 허용해도 인증된 TLS가 강제되지 않습니다. 많은 평문 프로토콜을 그 포트로 옮길 수 있기 때문입니다. 포트 80 차단은 다른 포트의 평문을 덮지 못합니다. 브라우저의 HTTPS 전용 모드도 그 브라우저에만 적용되며, 같은 기기의 별도 메일이나 채팅 앱에는 미치지 않습니다.

트래픽이 암호화된 것처럼 보이게 하려고 가로채기 인증서를 설치하는 것은 신뢰의 변경이지 수정이 아닙니다. 앱의 서버 인증을 사용자의 인증으로 바꿀 뿐이며, 처음부터 인증서를 검증하지 않던 앱은 그것으로 고쳐지지 않습니다. 누가 스트림을 읽을 수 있는지만 옮긴 셈입니다.

VPN이 도움이 되는 곳과 그렇지 않은 곳

터널은 로컬 네트워크가 연결에 대해 아무것도 모르게 해야 하거나, 앱이 다른 방법으로는 닿을 수 없는 사설 서비스에 도달해야 할 때 맞는 도구입니다. HTTP 클라이언트가 HTTPS를 쓰게 만드는 데는 틀린 도구입니다. 요청은 들어온 그대로 터널을 나가기 때문입니다.

Android에는 실무적 제약이 있습니다. 플랫폼은 한 번에 하나의 VPN 서비스만 실행합니다. 그 자리를 차지하는 로컬 필터링 앱은 실제로 필요한 원격 터널과 경쟁하며, 둘 사이의 전환은 앱 수준이 아니라 기기 수준의 작업입니다. 그 한 자리 때문에라도 페이로드 정책은 앱에 두고 터널은 경로용으로 남겨야 합니다.

둘이 겹치는 유일한 경우는 직접 운영하는 사설 서버입니다. 거기서는 프로토콜이 여전히 평문이어도 터널이 경로를 사설로 만들 수 있습니다. 노출은 줄지만 페이로드가 무엇인지는 바뀌지 않습니다. 위험 감소로 다루고, 데이터 암호화로 다루지 마십시오.

차단되었다고 부르기 전에 확인할 것

사람들을 놀라게 하는 기본값부터 시작하십시오. API 28 이상을 대상으로 하는 앱은 이미 평문을 거부해야 하므로, 기본값을 중복하는 구성을 추가하기 전에 대상 수준을 확인하십시오. 다음으로 실패 사례를 의도적으로 시험하십시오. 일반 HTTP 호스트로의 요청은 폴백으로 조용히 성공하는 것이 아니라 실패해야 합니다.

adb shell dumpsys package com.example.app | grep -E 'targetSdk|versionName'
adb logcat | grep -i cleartext

성공 사례에서는 HTTPS 호출이 실제 인증서 체인을 검증하는지, 신뢰할 수 없는 인증서를 가진 프록시가 허용되지 않고 거부되는지 확인하십시오. 어떤 인증서로도 성공하는 핸드셰이크는 전송 보안에 관해 아무것도 증명하지 못합니다.

타사 앱이라면 실제로 강제할 수 있는 경계를 적으십시오. 관리 기기 정책인지, 네트워크 규칙인지, 아니면 아무것도 없는지를 인수 기준에 명시하십시오. Android 보안 구성 가이드에는 플랫폼 기본값과 어떤 구성 요소가 정책을 따르는지가 나와 있으며, 구성이 실제로 의미가 있는지를 결정하는 부분이 바로 이것입니다.

관련 글