일반적으로 웹 개발에서 로컬 개발환경은 http://localhost
인 경우가 많습니다. 대개는 별다른 문제 없이 대부분의 기능을 사용하며 개발할 수 있는데, 이는 브라우저가 localhost
인 경우 보안 정책에 예외를 두는 등, TLS 통신과 동일한 보안 특성을 간주하고 HTTPS
처럼 동작하기 때문입니다.
정확하게는 localhost
및 .localhost
에 속하는 모든 도메인을 '잠재적으로 신뢰할 수 있는 출처(Potentially Trustworthy Origin)'으로 취급하는 것인데, 가령 Service Worker 라거나 Web Authentication API, Sensor API 같은 것들은 반드시 보안 컨텍스트(HTTPS
)에서만 동작한다는 제약사항이 존재하지만, localhost
에서는 예외적으로 정상적인 사용이 가능합니다.
5. 보안 고려 사항
...
5.2. localhost
RFC6761의 섹션 6.3은 localhost의 해석과 .localhost에 속하는 도메인을 특별하게 다루어야 함을 설명하며, 로컬 리졸버가 이를 특수하게 처리하도록 권고합니다. 그러나 리졸버들은 종종 이러한 제안을 무시하고, localhost를 네트워크로 전송하여 해석하는 경우가 많습니다.
따라서, 사용자 에이전트는 let-localhost-be-localhost에 명시된 이름 해석의 규칙을 준수하는 경우에만 잠재적으로 localhost를 신뢰할 수 있는 출처로 취급할 수 있습니다. 이는 결국 localhost가 다른 루프백 주소가 아닌 주소로 해석되지 않도록 보장하는 내용입니다.
- W3C, Secure Contexts
그러나, 막상 개발을 하다 보면 로컬에서도 HTTPS
여야만 하는 경우가 왕왕 있습니다. 흔하게는 Secure
속성을 사용한 쿠키, 즉 보안 쿠키(Secure cookie)를 사용하려는 때가 그렇습니다. 결국엔 개발 환경에서도 HTTPS
를 적용하는 편이 아무래도 더 편리한 셈인데, 이외에도 아래와 같은 경우가 해당될 수 있습니다.
- Mixed content 이슈를 디버깅해야 할 때
- HTTP/2 이상의 프로토콜을 사용하고자 할 때
- HTTPS가 요구되는 라이브러리, API 등을 사용할 때
- 호스트네임을 커스터마이즈했을 때
하지만 HTTPS
를 적용하려면 TLS/SSL
인증서가 필요합니다. 또한 인증서는 공인된 실제 인증 기관(Certificate Authority, CA)으로부터 서명된 것이어야 하며, 그래야만 브라우저가 유효한 인증서로 간주하게 됩니다.
로컬 개발환경에서 이런 실제 인증서를 사용하는 것은 설정부터가 다소 복잡한 데다, 유효한 도메인 이름을 사용해야만 하는 등(가령 localhost나 예약된 도메인 같은 것은 사용할 수 없습니다) 제약이 있어 여러모로 번거로울 수 있습니다.
아니면 그냥 개인적으로 자체 서명한 인증서를 생성하고, 그걸 브라우저가 허용하게 할 수도 있습니다만, 아래와 같이 브라우저에서 별도의 플래그 설정이 필요하고, 거슬리는 경고도 발생하게 됩니다.
때문에 결국엔 인증서를 직접 생성하되, 디바이스와 브라우저가 로컬에서 신뢰하는 CA를 사용하여 서명하는 방법이 가장 간단하고 깔끔합니다. 이는 무료로 인증서를 제공해주는 비영리 CA 기관으로 유명한 Let's Encrypt
에서도 권장하는 방법입니다.
이러한 목적으로 인증서를 생성할 때 가장 많이 사용되는 도구로는 mkcert
가 있는데, 자세한 사용 방법은 아래에서 이어서 살펴보겠습니다.
mkcert
설치는 매우 간단합니다만, OS에 따라 방법이 약간 다릅니다.
linux
에서는 먼저 인증서 관련 유틸리티 certutil
설치가 필요하므로, 아래의 패키지를 먼저 설치합니다. (배포판에 따라 구체적인 방법은 달라집니다)
sudo apt install libnss3-tools
OR
sudo yum install nss-tools
OR
sudo pacman -S nss
그런 다음 미리 빌드된 바이너리 패키지를 다운받습니다. mkcert
저장소를 클론하고 로컬에서 직접 빌드하는 방법도 있습니다만, Golang
세팅이 필요하고 구태여 번거로운 방법이므로 생략하겠습니다.
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
brew install mkcert
brew install nss
nss
패키지는 Firefox
를 사용하는 경우에 설치합니다.
Windows
에서는 아래와 같이 패키지 매니저인 chocolately
를 사용하여 설치할 수 있습니다.
choco install mkcert
아니면 릴리즈 페이지에서 그냥 exe
파일을 다운받는 것이 더 간단할 수 있습니다.
설치가 완료되었다면, 다음의 명령어로 로컬 CA를 만들고 Root CA 인증서를 생성합니다. Windows
에서는 루트 인증서 설치에 대한 보안 경고 다이얼로그가 나타나는데, 확인을 선택해주면 됩니다.
mkcert -install
생성된 루트 인증서(rootCA.pem
, rootCA-key.pem
)는 아래의 명령어로 위치를 확인할 수 있습니다. 가령 모바일 디바이스에서는 인증서 파일을 직접 옮겨서 설치해야 할 수 있으므로, 이런 경우 필요할 수 있습니다.
mkcert -CAROOT
⛔ 이렇게 생성한 루트 인증서는 공개된 저장소에 업로드하는 등 타인과 절대 공유해서는 안 됩니다! 요청을 가로채는 보안 공격의 수단으로 사용될 수 있기 때문입니다. 만약 해당 작업을 팀 단위로 진행하고 있는 경우라면, 팀원들 각자가 개별적으로 실행해야 합니다.
그런 다음 아래와 같이 지정한 호스트 도메인들에 대한 인증서를 생성합니다. 호스트는 각각 공백으로 구분하여 인수로 전달할 수 있고, 와일드카드도 가능합니다. 이 과정에서 mkcert
도 해당 인증서에 서명하게 됩니다.
mkcert "*.example.dev" localhost 127.0.0.1 ::1
별다른 추가 옵션을 명시하지 않았다면, 현재 명령어를 실행하고 있는 경로에 두 개의 .pem 파일(cert
, key
)이 생성되었을 것입니다.
이제 이 인증서를 적당한 디렉토리에 옮기고, 각 개발환경에 따라 웹 서버에서 해당 인증서를 사용하여 HTTPS
를 활성화하도록 설정해주어야 합니다. 모든 케이스를 예로 들기는 어렵지만, 일반적인 설정의 샘플을 몇 가지 나열해 보자면 아래와 같습니다. (경로는 모두 임의로 설정된 것이며, 기타 여러 설정들이 생략되어 있습니다)
# httpd-ssl.conf
<VirtualHost _default_:443>
DocumentRoot "/var/www/example"
ServerName example.dev:443
SSLEngine on
SSLCertificateFile "/usr/local/apache2/ssl/example.dev.pem"
SSLCertificateKeyFile "/usr/local/apache2/ssl/example.dev-key.pem"
...
</VirtualHost>
# nginx.conf
http {
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.dev www.example.dev;
root /var/www/example;
ssl_certificate ~/example.dev.pem;
ssl_certificate_key ~/example.dev-key.pem;
...
}
}
// server.js
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('./example.dev-key.pem'),
cert: fs.readFileSync('./example.dev.pem'),
};
https.createServer(options, (req, res) => {
// ...
}).listen(3000);
❗ http-server를 사용하고 있다면
http-server -S -C example.dev.pem -K example.dev-key.pem
Next
의 경우, 공식 문서의 custom server 섹션에서 설명하는 것처럼 서버 설정을 직접 커스터마이즈하는 방식으로 인증서를 사용하도록 설정할 수 있습니다. 프로젝트 루트에 server.js
를 생성한 다음, 위에서 먼저 예시로 들었던 Node
서버와 동일한 형태로 createServer()
에서 option으로 인증서 파일을 전달하여 재정의하는 것입니다.
하지만 이는 공식적으로 권장되지 않는 방법인데, 이렇게 서버를 변경하면 HMR(Hot Module Refresh)
기능에서 문제가 발생한다거나 자동 정적 최적화 같은 기능을 사용할 수 없게 되는 등 오히려 문제가 복잡해지기 때문입니다.
때문에 이런 케이스에서는 통상적인 사용례와 같이 앞단에 Nginx
를 세팅하거나, local-ssl-proxy
같은 도구를 사용하는 식으로 (리버스) 프록시를 통해 SSL을 상위 계층으로 올려 HTTPS를 적용하는 편이 훨씬 간단할 수 있습니다. 그 편이 관심사 분리의 측면에서도 적절하고, 성능적으로도 더 효율적인 것은 물론입니다.
next dev -p 3001 | local-ssl-proxy --key example.dev-key.pem --cert example.dev.pem --source 3000 --target 3001
다만 이렇게 프록시를 적용하는 경우에는, WebSocket
연결을 정상적으로 처리할 수 있는지 확인하여 필요한 경우 추가로 설정을 해줘야 할 수 있습니다. 개발 과정에서 중요하게 활용되는 기능 중 하나인 HMR
이 WebSocket으로 구현되기 때문입니다(Next 12
부터).
공식 문서에서는
Nginx
및Apache
로 프록시하는 경우 WebSocket Upgrade 요청을 설정하는 방법을 가이드하고 있습니다.
Nuxt
역시 Next
와 마찬가지로, 굳이 커스텀 서버를 변경하기보단 프록시를 세팅하는 편이 더 일반적이고 실무적인 측면에서도 바람직합니다. 다만 그럼에도 직접 설정하고자 한다면, 아래와 같이 할 수는 있습니다. Nuxt
(3 기준)에는 자체적으로 구동되는 서버 엔진(웹 서버 프레임워크)인 Nitro
의 설정을 아래와 같이 변경해주면 됩니다.
// nuxt.config.js
export default defineNuxtConfig({
devServer: {
https: {
key: './example.dev-key.pem',
cert: './example.dev.pem'
},
host: 'example.dev',
port: 3000
}
// ...
});
그러나 역시 WebSocket 연결을 고려해야 합니다. Nuxt
에서 기본적으로 채택하고 있는 번들러인 Vite
역시 웹소켓으로 HMR
을 구현하므로, 만약 연결에 실패하는 등의 이슈가 있는 경우 HMR 연결 설정과 관련하여 프로토콜, 포트 등의 설정을 직접 명시하거나 변경해야 할 수 있습니다.
// nuxt.config.js
export default defineNuxtConfig({
vite: {
server: {
hmr: {
protocol: 'wss',
host: 'example.dev'
}
}
}
// ...
});
설정이 완료되었다면, 웹 서버를 재기동한 다음 브라우저에서 확인해 봅니다. 아래와 같이 보인다면 정상적으로 적용된 것입니다.
📖 참고 문서