IIS HSTS 기술 및 적용

대부분의 웹 사이트는 보안성을 위해 HTTPS를 지원합니다. 상업적인 웹 사이트뿐만 아니라 개인 블로그까지 이제 HTTPS는 선택이 아니라 거의 강제에 가까운 옵션이라 할 수 있습니다. 최신 브라우저들은 HTTPS를 지원하지 않는 웹 사이트를 위험성이 있는 페이지로 표시하고 있으며, 검색 엔진 또한 HTTPS를 적용한 사이트를 검색 결과에 우선적으로 노출하고 있습니다.

적잖은 가격으로 부담이 되었던 SSL/TLS 인증서는 Let’s Encrypt와 같은 오픈 소스 기반의 무료 인증서나, 퍼블릭 클라우드 제공사와 CDN 사업자가 무료로 제공하는 인증서 등을 적용해 쉽게 사용할 수 있게 되었습니다.

그렇지만 아직도 HTTPS를 사용하지 않는 웹 사이트와 클라이언트는 존재합니다. HTTPS의 작동 원리를 살펴봤던 1편에 이어, 이번 글에서는 브라우저가 웹 서버와 통신 시, 무조건 HTTPS 통신을 할 수 있도록 수행하는 HSTS 기술에 대해 알아보겠습니다.

HSTS란 무엇인가?

HSTS(HTTP Strict Transport Security, RFC6797)는 웹 사이트 접속 시 HTTPS만 사용하도록 강제하는 기술입니다. HSTS가 적용된 웹 사이트의 웹 서버는 클라이언트에게 HTTPS만을 사용할 수 있음을 알려주고, HSTS를 지원하는 브라우저는 이를 해석하고 적용합니다. 대부분 최신 버전의 브라우저들은 모두 HSTS를 지원하고 있습니다.

HSTS를 사용하지 않는 경우, 일반적으로 HTTPS로의 접속 유도 방법은 웹 서버에 다음과 같은 페이지 재전송 설정을 만드는 것이었습니다.

① 사용자는 HTTP 방식으로 본인이 접속하려는 웹 사이트의 주소를 브라우저 주소창에 입력한다.
② 웹 서버는 HTTP 접속에 대해 보통 301 혹은 302 응답으로 HTTPS 사이트로 페이지를 재전송한다.
③ 사용자의 브라우저는 비로소 안전한 HTTPS 방식으로 다시 웹 서버에 접속한다.​

위의 방법은 잘 동작하지만 사실 ①번 단계에서 사용자는 이미 HTTP 접속을 요청했기 때문에, 사용자와 같은 네트워크 상에서 사설 프록시나 해킹 도구를 운영하는 해커들은 중간자 공격의 형태로 사용자의 HTTP 패킷을 몰래 캡처하고, 쿠키 값 혹은 세션 정보 등의 민감한 사용자 데이터를 엿볼 수 있습니다.

HSTS를 적용하는 방법

HSTS를 적용한 도메인은 사용자가 처음으로 웹 서버에 접속할 때 응답 헤더에 Strict-Transport-Security라는 헤더를 내려보내고, HSTS를 지원하는 브라우저라면 추후 접속부터는 HTTPS로만 접속합니다.

Strict-Transport-Security 응답 헤더는 다음과 같이 설정합니다.

Strict-Transport-Security: max-age=[적용 주기]
max-age 값은 초(second) 단위며, 해당 시간 동안 HSTS 응답을 받은 웹 사이트에 대해 HTTPS 접속만을 허용한다는 의미 
Strict-Transport-Security: max-age=[적용 주기]; includeSubDomainsHSTS가 해당 도메인의 서브 도메인에도 적용되고 있음을 알려줌 
Strict-Transport-Security: max-age=[적용 주기]; preload브라우저의 Preload List에 해당 도메인을 추가할 것을 알려줌

HSTS Preload List를 통한 간편한 적용

HSTS Preload List란 HSTS가 적용된 웹 사이트의 명단을 모아둔 리스트 정보입니다. HSTS Prelist List에 포함된 사이트들은 그 사이트를 처음 접속하는 브라우저, 즉 사전에 Strict-Transport-Security 응답 헤더를 받은 적이 없어도 HTTPS로만 접속합니다. 한 번이라도 접속해야만 적용 가능한 HSTS를 첫 방문부터 적용하여, HTTPS 만으로 접속하기 위해 만들어진 리스트라는 의미입니다.

이 리스트는 구글이 운영하는 hstspreload.org 사이트에 등재되어 있는데요. 이 사이트는 크롬 브라우저만의 HSTS Preload List를 서비스합니다. 여기에 등록된 사이트는 크롬 브라우저 내부에 하드 코딩되어 HTTPS로만 접속 가능합니다. 사파리 혹은 엣지 브라우저도 hstspreload.org에 기반한 자체적인 HSTS Preload List를 운영합니다. 

특정 도메인의 HSTS preload 상태 확인하기 <출처: hstspreload.org>

또한 위 사이트를 통해 어떤 도메인이 리스트에 포함되었는지 조회가 가능한데요. 앞에서 테스트한 것처럼 페이스북의 등록 여부를 조회할 수 있습니다.

네이버의 경우 리스트에는 없지만 서비스를 살펴보면, HSTS가 적용되어 관련 응답 헤더가 존재합니다. 아래 테스트에서 알 수 있듯이 HSTS를 사용하지만 아직 리스트 명부에는 없는 상태입니다.

curl -v –silent https://www.naver.com/ 2>&1 | grep strict
< strict-transport-security: max-age=63072000; includeSubdomains

위의 값을 해석하면 네이버의 HSTS 적용 주기는 63072000초(730일)이며, includeSubdomains 옵션을 통해 서브 도메인들까지 모두 HSTS가 적용되도록 설정되어 있습니다.

HSTS Preload List에 등록되지 않은 이유로는 http://naver.com로 접속 시, https가 아닌, http://www.naver.com으로 페이지가 재전송되기 때문일 수도 있습니다. 실제로 HSTS Preload List에 등록 후 SSL 인증서 만료 등으로 HTTPS 사용에 문제가 생기면 서비스에 아예 접속을 못하는 상황이 생길 수도 있고, HTTP 프로토콜로 다운그레이드를 하려면 리스트에서 도메인을 삭제하는 데 많은 시간이 걸립니다. 그래서 도메인 관리자는 리스트 등록 여부를 신중하게 결정해야 합니다.

HSTS 적용 고려하기

지금까지 안전한 웹을 위해 HTTPS 사용을 강제하면서, 보안 취약성은 제거한 HSTS 기술과 적용 방법에 대해 알아보았습니다. 현재 개인 혹은 회사 사이트에 HTTPS는 적용했지만, 아직 HSTS 혹은 HSTS Preload List를 적용하지 않았다면, 지금이라도 적용 여부를 고려해 안전한 웹 사이트 환경을 만드시길 바랍니다.


https://learn.microsoft.com/ko-kr/iis/configuration/system.applicationhost/sites/site/hsts

개요

<hsts> 요소의 <site> 요소에는 IIS 10.0 버전 1709 이상에서 사이트에 대한 HSTS(HTTP Strict Transport Security) 설정을 구성할 수 있는 특성이 포함되어 있습니다.

 참고

<hsts> 요소가 특정 사이트에 대한 섹션과 <site> 섹션 모두에서 <siteDefaults> 구성된 경우 섹션의 <site> 구성이 해당 사이트에 사용됩니다.

호환성

테이블 확장

버전참고
IIS 10.0 버전 1709<hsts> 요소의 <site> 요소는 IIS 10.0 버전 1709에서 도입되었습니다.
IIS 10.0해당 없음
IIS 8.5해당 없음
IIS 8.0해당 없음
IIS 7.5해당 없음
IIS 7.0해당 없음
IIS 6.0해당 없음

설치 프로그램

<hsts> 요소의 <site> 요소는 IIS 10.0 버전 1709 이상의 기본 설치에 포함됩니다.

방법

IIS 10.0 버전 1709에 <site> 대한 요소의 요소를 구성할 <hsts> 수 있는 사용자 인터페이스가 없습니다. 요소의 <site> 요소를 프로그래밍 방식으로 구성하는 <hsts> 방법에 대한 예제는 이 문서의 샘플 코드 섹션을 참조하세요.

특성

attributeDescription
enabled선택적 부울 특성입니다.

사이트에 대해 HSTS를 사용할지(true) 또는 사용 안 함(false)인지 지정합니다. HSTS를 사용하도록 설정하면 IIS가 웹 사이트에 HTTPS 요청을 회신할 때 Strict-Transport-Security HTTP 응답 헤더가 추가됩니다.

기본값은 false입니다.
max-age선택적 uint 특성입니다.

Strict-Transport-Security HTTP 응답 헤더 필드 값에 max-age 지시문을 지정합니다.

기본값은 0입니다.
includeSubDomains선택적 부울 특성입니다.

includeSubDomains 지시문이 Strict-Transport-Security HTTP 응답 헤더 필드 값에 포함되는지 여부를 지정합니다.

참고: 모든 하위 도메인이 실제로 TLS/SSL을 통해 HTTP 기반 서비스를 제공하는 경우에만 이 특성을 사용하도록 설정합니다.

기본값은 false입니다.
preload선택적 부울 특성입니다.

Preload 지시문이 Strict-Transport-Security HTTP 응답 헤더 필드 값에 포함되는지 여부를 지정합니다.

참고: 사이트의 도메인이 HSTS 사전 로드 목록에 포함되도록 제출된 경우에만 이 특성을 사용하도록 설정합니다.

기본값은 false입니다.
redirectHttpToHttps선택적 부울 특성입니다.

사이트에 대해 HTTP-HTTPS 리디렉션을 사용할지(true) 또는 사용 안 함(false)인지 지정합니다.

참고:redirectHttpToHttps 를 사용하도록 설정하면 사이트 수준 HTTP에서 HTTPS로 리디렉션이 적용됩니다. IIS는 HTTP 요청을 리디렉션할 때 URI 체계를 “https”로 바꾸고 포트 구성 요소를 무시합니다. 리디렉션 대상이 표준 포트 443에서 TLS/SSL을 통해 HTTP 기반 서비스를 제공하는지 확인합니다.

기본값은 false입니다.

구성 샘플

다음 구성 샘플에서는 HTTP 및 HTTPS 바인딩 모두에서 HSTS를 사용하도록 설정된 Contoso라는 웹 사이트를 보여 줍니다. max-age 특성은 31536000초(1년)로 설정되므로 사용자 에이전트는 Strict-Transport-Security 헤더 필드가 수신된 후 1년 이내에 호스트를 알려진 HSTS 호스트로 간주합니다. includeSubDomains 특성은 HSTS 정책이 이 HSTS 호스트(contoso.com)와 하위 도메인(예www.contoso.com: 또는 marketing.contoso.com)에 적용되도록 지정하기 위해 true로 설정됩니다. 마지막으로, 사이트에 대한 모든 HTTP 요청이 HTTPS로 리디렉션되도록 redirectHttpToHttps 특성이 true 로 설정됩니다.

XML복사

<site name="Contoso" id="1">
    <application path="/" applicationPool="Contoso">
        <virtualDirectory path="/" physicalPath="C:\Contoso\Content" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:80:contoso.com" />
        <binding protocol="https" bindingInformation="*:443:contoso.com" sslFlags="0" />
    </bindings>
    <hsts enabled="true" max-age="31536000" includeSubDomains="true" redirectHttpToHttps="true" />
</site>
<a href="https://learn.microsoft.com/ko-kr/iis/configuration/system.applicationhost/sites/site/hsts#sample-code"></a>

샘플 코드

다음 코드 샘플은 HTTP 및 HTTPS 바인딩을 모두 사용하여 Contoso라는 웹 사이트에 대해 HSTS를 사용하도록 설정합니다. 이 샘플에서는 max-age 특성을 31536000초(1년)로 설정하고 includeSubDomains 및 redirectHttpToHttps 특성을 모두 사용하도록 설정합니다.

AppCmd.exe

콘솔복사

appcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.enabled:True" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.max-age:31536000" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.includeSubDomains:True" /commit:apphost
appcmd.exe set config -section:system.applicationHost/sites "/[name='Contoso'].hsts.redirectHttpToHttps:True" /commit:apphost

 참고

AppCmd.exe 사용하여 이러한 설정을 구성할 때 commit 매개 변수 apphost 를 로 설정해야 합니다. 그러면 구성 설정이 applicationHost.config 파일의 적절한 위치 섹션에 커밋됩니다.

C#

using System;
using System.Text;
using Microsoft.Web.Administration;

internal static class Sample
{
    private static void Main()
    {
        using(ServerManager serverManager = new ServerManager())
        { 
            Configuration config = serverManager.GetApplicationHostConfiguration();
            ConfigurationSection sitesSection = config.GetSection("system.applicationHost/sites");
            ConfigurationElementCollection sitesCollection = sitesSection.GetCollection();
            
            ConfigurationElement siteElement = FindElement(sitesCollection, "site", "name", @"Contoso");
            if (siteElement == null) throw new InvalidOperationException("Element not found!");

            ConfigurationElement hstsElement = siteElement.GetChildElement("hsts");
            hstsElement["enabled"] = true;
            hstsElement["max-age"] = 31536000;
            hstsElement["includeSubDomains"] = true;
            hstsElement["redirectHttpToHttps"] = true;

            serverManager.CommitChanges();
        }
    }
    
    private static ConfigurationElement FindElement(ConfigurationElementCollection collection, string elementTagName, params string[] keyValues)
    {
        foreach (ConfigurationElement element in collection)
        {
            if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase))
            {
                bool matches = true;
                for (int i = 0; i < keyValues.Length; i += 2)
                {
                    object o = element.GetAttributeValue(keyValues[i]);
                    string value = null;
                    if (o != null)
                    {
                        value = o.ToString();
                    }
    
                    if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase))
                    {
                        matches = false;
                        break;
                    }
                }
                if (matches)
                {
                    return element;
                }
            }
        }
        return null;
    }
}
<a href="https://learn.microsoft.com/ko-kr/iis/configuration/system.applicationhost/sites/site/hsts#vbnet"></a>

VB.NET

Imports System
Imports System.Text
Imports Microsoft.Web.Administration

Module Sample

   Sub Main()
      Dim serverManager As ServerManager = New ServerManager
      Dim config As Configuration = serverManager.GetApplicationHostConfiguration
      Dim sitesSection As ConfigurationSection = config.GetSection("system.applicationHost/sites")
      Dim sitesCollection As ConfigurationElementCollection = sitesSection.GetCollection

      Dim siteElement As ConfigurationElement = FindElement(sitesCollection, "site", "name", "Contoso")
      If (siteElement Is Nothing) Then
         Throw New InvalidOperationException("Element not found!")
      End If

      Dim hstsElement As ConfigurationElement = siteElement.GetChildElement("hsts")
      hstsElement("enabled") = True
      hstsElement("max-age") = 31536000
      hstsElement("includeSubDomains") = True
      hstsElement("redirectHttpToHttps") = True

      serverManager.CommitChanges()
   End Sub

   Private Function FindElement(ByVal collection As ConfigurationElementCollection, ByVal elementTagName As String, ByVal ParamArray keyValues() As String) As ConfigurationElement
      For Each element As ConfigurationElement In collection
         If String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase) Then
            Dim matches As Boolean = True
            Dim i As Integer
            For i = 0 To keyValues.Length - 1 Step 2
               Dim o As Object = element.GetAttributeValue(keyValues(i))
               Dim value As String = Nothing
               If (Not (o) Is Nothing) Then
                  value = o.ToString
               End If
               If Not String.Equals(value, keyValues((i + 1)), StringComparison.OrdinalIgnoreCase) Then
                  matches = False
                  Exit For
               End If
            Next
            If matches Then
               Return element
            End If
         End If
      Next
      Return Nothing
   End Function

End Module
<a href="https://learn.microsoft.com/ko-kr/iis/configuration/system.applicationhost/sites/site/hsts#javascript"></a>

JavaScript

var adminManager = new ActiveXObject('Microsoft.ApplicationHost.WritableAdminManager');
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST";
var sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST");
var sitesCollection = sitesSection.Collection;

var siteElementPos = FindElement(sitesCollection, "site", ["name", "Contoso"]);
if (siteElementPos == -1) throw "Element not found!";
var siteElement = sitesCollection.Item(siteElementPos);

var hstsElement = siteElement.ChildElements.Item("hsts");
hstsElement.Properties.Item("enabled").Value = true;
hstsElement.Properties.Item("max-age").Value = 31536000;
hstsElement.Properties.Item("includeSubDomains").Value = true;
hstsElement.Properties.Item("redirectHttpToHttps").Value = true;

adminManager.CommitChanges();

function FindElement(collection, elementTagName, valuesToMatch)
{
    for (var i = 0; i < collection.Count; i++)
    {
        var element = collection.Item(i);
        if (element.Name == elementTagName)
        {
            var matches = true;
            for (var iVal = 0; iVal < valuesToMatch.length; iVal += 2)
            {
                var property = element.GetPropertyByName(valuesToMatch[iVal]);
                var value = property.Value;
                if (value != null)
                {
                    value = value.toString();
                }
                if (value != valuesToMatch[iVal + 1])
                {
                    matches = false;
                    break;
                }
            }
            if (matches)
            {
                return i;
            }
        }
    }
    
    return -1;
}

VBScript

Set adminManager = WScript.CreateObject("Microsoft.ApplicationHost.WritableAdminManager")
adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST"
Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST")
Set sitesCollection = sitesSection.Collection
siteElementPos = FindElement(sitesCollection, "site", Array("name", "Contoso"))

If siteElementPos = -1 Then
   WScript.Echo "Element not found!"
   WScript.Quit
End If

Set siteElement = sitesCollection.Item(siteElementPos)
Set hstsElement = siteElement.ChildElements.Item("hsts")
hstsElement.Properties.Item("enabled").Value = True
hstsElement.Properties.Item("max-age").Value = 31536000
hstsElement.Properties.Item("includeSubDomains").Value = True
hstsElement.Properties.Item("redirectHttpToHttps").Value = True

adminManager.CommitChanges()

Function FindElement(collection, elementTagName, valuesToMatch)
   For i = 0 To CInt(collection.Count) - 1
      Set element = collection.Item(i)
      If element.Name = elementTagName Then
         matches = True
         For iVal = 0 To UBound(valuesToMatch) Step 2
            Set property = element.GetPropertyByName(valuesToMatch(iVal))
            value = property.Value
            If Not IsNull(value) Then
               value = CStr(value)
            End If
            If Not value = CStr(valuesToMatch(iVal + 1)) Then
               matches = False
               Exit For
            End If
         Next
         If matches Then
            Exit For
         End If
      End If
   Next
   If matches Then
      FindElement = i
   Else
      FindElement = -1
   End If
End Function
<a href="https://learn.microsoft.com/ko-kr/iis/configuration/system.applicationhost/sites/site/hsts#iisadministration-powershell-cmdlets"></a>

IIS관리 PowerShell Cmdlet

Import-Module IISAdministration
Reset-IISServerManager -Confirm:$false
Start-IISCommitDelay

$sitesCollection = Get-IISConfigSection -SectionPath "system.applicationHost/sites" | Get-IISConfigCollection
$siteElement = Get-IISConfigCollectionElement -ConfigCollection $sitesCollection -ConfigAttribute @{"name"="Contoso"}
$hstsElement = Get-IISConfigElement -ConfigElement $siteElement -ChildElementName "hsts"
Set-IISConfigAttributeValue -ConfigElement $hstsElement -AttributeName "enabled" -AttributeValue $true
Set-IISConfigAttributeValue -ConfigElement $hstsElement -AttributeName "max-age" -AttributeValue 31536000
Set-IISConfigAttributeValue -ConfigElement $hstsElement -AttributeName "includeSubDomains" -AttributeValue $true
Set-IISConfigAttributeValue -ConfigElement $hstsElement -AttributeName "redirectHttpToHttps" -AttributeValue $true

Stop-IISCommitDelay
Remove-Module IISAdministration

Leave a Comment