사이트 보안 강화 팁

웹 사이트 보안을 웹 서버만 사용해서 향상할 수 있는 방법들을 소개합니다.

나는 사실 외부인에게 공개되는 것을 목적으로 하는 웹 사이트를 운영하는 것은 처음이다. 그래서 보안을 더 신경써야하는 상황이 되었는데, 자신이 없었다. 이전에 회사 사이트를 운영할 때에도 PHP, Wordpress 취약점을 통해서 해킹 공격이 들어왔었던 기억이 있다. 일본 IP로 사이트를 접속하면 광고가 뜨는 식이라서 오랜 기간 몰랐었던 기억이 난다. 그래서 내가 운영하는 사이트만큼은 보안에 신경을 쓰고 싶었다.

나는 직접 프로그래밍을 통해서 운영하는 것이 아니기 때문에 보안에 신경써야하는 부분은 웹 서버 단의 문제이다. 그래서 이 글은 웹 서버 단에서 강화할 수 있는 내용을 중심으로 작성이 되어 있다.

환경

  • Nginx 1.27.2

HTTPS만 사용하기

여러 검색 플랫폼에서도 플러스 점수가 되는 요인 중 하나인데, HTTPS로만 접속을 허용하는 것이다. 일반적으로 HTTP 요청을 HTTPS로 리다이렉트 설정함으로서 해결한다. Nginx에서 설정은 아래와 같이할 수 있다.

server {
    listen 80;
    listen [::]:80;

    server_name <Domain>;

    location / {
        return 301 https://$host$request_uri;
    }
}

location 부분을 참고하면 된다. 모든 endpoint의 요청을 301로 응답하여, 컨텐츠들이 HTTPS로만 제공됨을 알린다.

301로 응답해야 검색 엔진 크롤링 봇이 URL을 업데이트한다. 302로 응답할 경우 URL을 업데이트하지 않는다. (출처: MDN)

HTTP/2 활성화

HTTP/2는 HTTP/1.1에서 속도가 향상된 프로토콜이다. 중복 Header 생략 등의 기능이 있다. Nginx 에서는 사용하겠다고 명시가 되어 있지 않으면 HTTP/1.1로 정보를 제공한다. Nginx 에서 명시적으로 활성화하는 방법은 아래와 같다.

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    # HTTP/2 활성화
    http2 on;

    ssl_certificate     <SSL Cert Location>;
    ssl_certificate_key <SSL Cert Key Location>;

    location / {
        return 301 https://$host$request_uri;
    }
}

많은 글에서는 listen 구문에 'http2'라고 기재하는 경우가 많은데, 최신 버전에서는 'http2 on' 이라고 기재해야 한다.

TLSv1.2, v1.3만 허용

TLS v1.0, v1.1는 취약점으로 인해서 최신 브라우저에서 기본적으로 비활성화 되어있는 프로토콜이다. TLS 버전에 대한 각종 정보는 여기에서 확인할 수 있다. Nginx에서는 별도로 선언하지 않으면 모든 TLS 버전을 지원하게 되며, 특정 버전만 지원한다고 선언을 해주어야 한다.

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_certificate     <SSL Cert Location>;
    ssl_certificate_key <SSL Cert Key Location>;
    # TLSv1.2, 1.3만 허용
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        return 301 https://$host$request_uri;
    }
}

만일, 본인 사이트가 이 설정이 제대로 적용되었는지 확인하고 싶다면 여기에서 본인의 사이트를 테스트할 수 있다. (단, Cloudflare Proxy 등이 적용되어 있다면 테스트 결과는 명확하지 않을 수 있다.)

Get 요청만 허용

일부 API를 제외하고 브라우저에게 보여주는 페이지들은 대부분 Get 요청만 필요할 것이다. 불필요하게 다른 Method로 요청이 오는 경우는 공격일 가능성이 있으므로 나는 Get 요청만 허용하도록 설정하였다. 이 설정은 아래와 같이 할 수 있다.

server {
    listen 80;
    listen [::]:80;
    server_name <Domain>;

    location / {
        # 이 설정이 핵심이다.
        limit_except GET {
            deny all;
        }

        return 301 https://$host$request_uri;
    }
}

CORS 설정

CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 다른 출처의 리소스에 대해 요청을 보낼 때, 서버가 이를 허용할지 결정한다. 이는 서버가 브라우저의 요청 출처를 확인하고 허용 여부를 결정함으로써, 불필요한 요청이나 데이터 탈취를 방지한다. CORS는 클라이언트-서버 간의 리소스 접근을 제어하며, 서버 간의 리소스 실행 제어를 목표로 하는 CSP(Content Security Policy)와는 역할이 다르다.

Nginx에서 CORS를 설정하기 위해서는 별도로 header를 추가하는 식으로 대응할 수 있다. 아래와 같이 설정할 수 있다.

server {
    listen 80;
    listen [::]:80;
    server_name <Domain>;

    add_header Access-Control-Allow-Origin "https://example.com";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Headers "Content-Type, Authorization";

    location / {
        # 이 설정이 핵심이다.
        limit_except GET {
            deny all;
        }

        return 301 https://$host$request_uri;
    }
}

CSP(Content Security Policy) 설정

CSP(Content Security Policy)는 웹 사이트에서 제공하는 컨텐츠의 src를 제한하는 Header 이다. 이 설정을 통해서 XSS 공격 등을 방어할 수 있다.

만일, 웹 페이지를 작성할 때 font, css 등을 외부 CDN 에서 갖고 오는 식으로 작성하거나 HTML Element 에 JavaScript 코드나 CSS Style 을 정의했다면, 이를 모두 걷어내고 별도의 파일로 작성하고 link 등을 통해서 정의하여야 한다.

이게 Express, Spring Security 등 개발 단계에서 사용하는 Framework에서 정의하게 된다면 API를 사용해서 가독성 있도록 작성할 수 있다. 하지만, Nginx에서는 한 줄로만 정의되어야 하다보니 작성하기가 많이 힘든 부분이 있다. 그래서 Nginx 등 웹 서버만으로 CSP를 정의해야 한다면, 최대한 허용하는 경우가 없는 쪽이 좋을 것 같다.

CSP으로 지정할 수 있는 종류는 아래와 같다.

  • deafult: 아래의 것들을 정의하지 않는다면 기본적으로 여기의 정책을 따른다.
  • script-src: <script> 태그와 외부 소스를 제한
  • img-src: 이미지 파일의 출처를 제한
  • media-src: 오디오/비디오 파일의 출처를 제한
  • font-src: 웹 폰트의 출처를 제한
  • connect-src: Ajax 요청, WebSocket, fetch() 등의 연결처 제한
  • object-src: <object>, <embed>, <applet>등의 출처를 제한
  • frame-src: <iframe>의 출처를 제한
  • worker-src: WebWorkers, Shared Workers의 출처 제한
  • manifest-src: Web Application의 manifest 파일의 출처 제한
  • form-action: <form> 제출 시 허용되는 출처 제한
  • frame-ancestors: 이 사이트를 embed 할 수 있는 출처 제한
  • sandbox: 문서를 제한된 sandbox 환경에서 실행
  • base-uri: <base> 태그의 출처 제한

이 종류에 정의할 수 있는 값은 아래와 같다.

  • 'self': 같은 출처일 경우 허용
  • 'none': 아무 것도 허용하지 않음
  • 'unsafe-inline': 인라인 스크립트나 스타일을 허용
  • 'unsafe-eval': eval() 호출을 허용
  • 'data:' 'data:' 구문을 허용
  • 'https:': https 출처만 허용
  • <URL>: 특정 URL을 지정

예를 들면 아래와 같이 지정할 수 있다.

default-src 'self' data:; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com;

일부 확장자, 문자열 접근 차단

해킹 위험을 방지하기 위해서 일단 서버 Side에서 실행되는 파일의 확장자들 .php 등을 막았다. 그리고, 실제로 apache, wordpress를 사용하는게 아님에도 Apache 에서 사용하는 일부 endpoint와 Wordpress에서 사용하는 Endpoint도 막아 두었다.

운영하면서 access log를 보고 어떤 endpoint에서 공격이 들어오는지 보고 막으면 되겠다. 이게 Response code를 보고 어떤 웹 서버인지 어떤 CMS인지 알아내서 취약점으로 공격하는 것으로 알고 있어서 애초부터 차단하는게 좋을 것 같다.

robots.txt 설정

robots.txt의 경우에는 대부분 검색 엔진 최적화에 쓰인다. 하지만, 검색 엔진에 캐싱되면 안 되는 것들이 종종 있다. 그러므로 이 부분을 robots.txt로 제어할 필요가 있다.

나 같은 경우에는 static 파일들과 admin 페이지를 검색 엔진에 들어가지 않도록 설정했다.

무료 검사기 - Mozilla HTTP Observatory

이런 설정들이 제대로 적용되었는지 확인할 수 있는 사이트가 있다. 바로 Mozilla HTTP Observatory 이다. 이는 Response Header와 Meta 기반으로 확인하는 것으로 보여서 다른 취약점까지 확인할 수 있는 것은 아니다. 정말 기본이 되어 있는지를 확인하면 된다.