Scala/Play framework

Let's Encrypt로 인증서 발급받고 적용하기

partner_jun 2017. 7. 13. 15:57

https는 추가적인 암호화 과정이 필요해 메모리를 더 사용하고 페이지 로딩이 느려지는 경향이 있다. 그럼에도 이제 대부분의 사이트는 https를 사용한다.


https에 사용되는 인증서는 SSL 인증서를 구매해야 하는데, 이전 검색해 보았을 때 COMODO의 가장 싼 인증서가 1년에 3만원이었던 것 같다. 비싼 가격은 아니지만 결제해야 된다는 것 자체가 부담스럽다. 그런 인증서를 Let's Encrypt를 통해 무료로 발급받아 사용할 수 있다. 물론 이런저런 제한90일마다 갱신해야한다는 귀찮은 점은 감수해야 한다.


Play framework에서 Let's Encrpy로 발급받은 인증서를 적용하려면 몇 가지 과정이 필요하다. 이전 Netty 서버에 적용할때도 한참 고생했던 기억이 있다. 발급받는 곳마다 인증서 파일의 이름도 통일되어 있지 않고, 형식도 다르다. 정말 골치 아픈 일이다.


아무튼, Let's Encrypt로 인증서를 발급받으려면 먼저 Linux OS와 OpenSSL 설치가 필요하다. 이 포스트는 Ubuntu에서 진행했다. 붉게 표시한 명령어는 설정에 따라, 취향에 따라 바꿀 수 있는 부분이다.




0. Let's Encrypt 설치

sudo git clone https://github.com/letsencrypt/letsencrypt /home/letsencrypt

git에서 Let's Encrypt 프로젝트를 다운받는다.


cd /home/letsencrypt


./letsencrypt-auto

다운받은 폴더로 이동하고 letsencrypt-auto를 실행해 의존성 패키지를 다운로드 받는다.




1. 인증서 발급받기

./letsencrypt-auto certonly --standalone -n -m email@example.com --agree-tos -d example.com -d www.example.com

or

./letsencrypt-auto certonly --manual

발급받을 URL을 입력하면 URL의 하위 URL(예를 들면 example.com/.well-known/acme-challenge/12345 같은 주소)에 접속했을 때 특정 문자열을 반환하게 만들라는 안내 메시지가 출력된다. 

간단한 웹 어플리케이션을 작성하거나 툴을 이용해 서버에서 해당 문자열을 반환하도록 한 후 엔터를 눌러 발급받자.

한번에 할 필요는 없지만 일정 시간이 지나면 URL이 변하므로 주의하자.

또, 발급받을때 적는 이메일로 갱신 알람이 오니 정확히 작성하자.




2. pem -> pkcs12 변환

cd /etc/letsencrypt/live/example.com

먼저 발급받은 인증서가 있는 폴더로 이동한다. 발급받은 사이트 이름에 해당하는 폴더가 live 폴더 아래에 생성된다.


openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out cert_and_key.p12 -CAfile chain.pem -caname root -passout PASSWORD

-canme은 Root CA의 이름, -passout은 pkcs12의 비밀번호를 입력한다.

openssl을 이용해 .pem 파일들을 .p12 파일로 변환한다.




3. pkcs12 -> jks로 변환

keytool -importkeystore -srcstorepass PASSWORD -destkeystore keyStore.jks -srckeystore cert_and_key.p12 -srcstoretype PKCS12 -storepass PASSWORD

-srcstorepass는 jks의 비밀번호, -storepass는 pkcs12의 비밀번호를 입력한다.

java의 keytool을 이용해 .p12 파일을 .jks 파일로 변환한다.




4. SslEngine class 작성 (Stackoverflow의 이 답변 참조)

기본 웹 페이지뿐 아니라 Websocket등의 Akka 서비스를 위해서 SSLEngine 클래스를 작성하고 적용해야 한다. 이 클래스에서 사용하는 옵션은 공식 홈페이지와 API를 참조하자. (사실 너무 간략하게 적혀있다.)

import java.nio.file._
import java.security.KeyStore
import javax.net.ssl._

import play.core.ApplicationProvider
import play.server.api._

class CustomSslEngineProvider(appProvider: ApplicationProvider) extends SSLEngineProvider {

val priorityCipherSuites = List(
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA")


def readPassword(): Array[Char] = System.getProperty("play.server.https.keyStore.password").toCharArray

def readKeyInputStream(): java.io.InputStream = {
val keyPath = FileSystems.getDefault.getPath(System.getProperty("play.server.https.keyStore.path"))
Files.newInputStream(keyPath)
}

def readKeyManagers(): Array[KeyManager] = {
val password = readPassword()
val keyInputStream = readKeyInputStream()
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
keyStore.load(keyInputStream, password)
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
kmf.init(keyStore, password)

kmf.getKeyManagers
} finally {
keyInputStream.close()
}
}

def createSSLContext(): SSLContext = {
val keyManagers = readKeyManagers()
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, Array.empty, null)
sslContext
}

override def createSSLEngine(): SSLEngine = {
val ctx = createSSLContext()
val sslEngine = ctx.createSSLEngine
val cipherSuites = sslEngine.getEnabledCipherSuites.toList
val orderedCipherSuites =
priorityCipherSuites.filter(cipherSuites.contains) ::: cipherSuites.filterNot(priorityCipherSuites.contains)
sslEngine.setEnabledCipherSuites(orderedCipherSuites.toArray)
val params = sslEngine.getSSLParameters
params.setUseCipherSuitesOrder(true)
sslEngine.setSSLParameters(params)
sslEngine
}
}

 application.conf  파일에 만든 SslEngine class를 적용한다.

play.server.https.engineProvider= modules.CustomSslEngineProvider



5. http -> https Redirect 필터

http로 접속했을 때 https로 리다이렉트하게 하고자 한다면 필터를 작성하고 적용해야 한다.

class HttpsRedirectFilter @Inject()(implicit val mat: Materializer,
implicit val ec: ExecutionContext) extends Filter {

override def apply(f: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = {
if(!rh.secure) {
Future.successful(Results.Redirect("https://" + rh.host + rh.uri, 301))
} else f(rh)
}

}

 application.conf  파일에 만든 필터를 적용한다.

play.http.filters = filter.HttpRedirectFilter




6. 인증서를 적용하고 웹 어플리케이션 구동

이제 만든 .jks 파일을 이용해 웹 어플리케이션을 구동한다.  application.conf  파일에 작성하지 않았으므로 실행할 때 http, https 포트를 설정하고 .jks 파일의 위치, 비밀번호를 적어야 한다.

./server -Dhttp.port=80 -Dhttps.port=443 -Dplay.server.https.keyStore.path=/home/example/keyStore.jks -Dplay.server.https.keyStore.password=PASSWORD