지피지기 백전백퇴

Nix + Caddy 서버에서 CORS 설정하기


개요: 여러곳에서 사용할 수 있는 API 서버를 만들기가 쉽지 않다.

간단하게 말하자면 웹표준을 준수하는 CORS를 만들기가 꽤 번거롭다.

처음에는 그냥 access-control-allow-... 헤더를 적당히 설정하면 CORS는 해결되는 건줄 알았다:

access-control-allow-origin *
access-control-allow-methods *
access-control-allow-headers "origin,content-type,accept"

정도만 설정해도 행복했었는데, 나중에 인증 붙이려고 보니까…

  1. access-control-allow-origin: *인 경우, credentials가 전달되지 않음
    글을 쓰면서 생각해보니 이걸 허용하면 XSS 공격이 들어왔을 때 현재 브라우저의 인증 정보가 마구 유출될 수 있어서 불가피한 선택으로 보인다.
  2. 따라서 access-control-allow-origin에 내가 허용하는 도메인 목록을 나열해서 적어줘야 하는데, 그럼…
    • 개발은 어떻게 하나? http://localhost:3000을 허용해야 하나?
    • 내가 XSS 구겨넣은 앱에서 접근하는건 또 origin: null이라는 황당한 헤더인데, 이런것도 allow시켜주면 되는건가?
  3. 그리고 ...-allow-origin에 여러개의 허용 사이트 목록을 보내줄 수 있는지도 모르겠지만 보내선 안될 것 같은 느낌이 들었다. 따라서 요청의 origin이 맞는 경우에만 거기에 맞춰서 CORS 헤더를 반환하도록 해야 했다. 바꿔말하면 서버가 요청의 헤더값을 참조해서 매칭을 수행해야 한다는 것.

결론적으로, Caddy 서버를 NixOS에서 설정하는 것 기준으로 다음과 같은 설정 파일이 만들어졌다.

{ pkgs, ... }: {
    services.caddy =
    {
        enable = true;
        extraConfig = ''
            (cors) {
                @origin{args.0} header Origin {args.0}
                header @origin{args.0} Access-Control-Allow-Origin "{args.0}";
                header @origin{args.0} Access-Control-Allow-Credentials "true";
                header @origin{args.0} Access-Control-Allow-Methods *;
                header @origin{args.0} Access-Control-Allow-Headers "Origin,Content-Type,Accept,Authorization";
            }
        '';

        virtualHosts = {
            "gql-service.example.com".extraConfig = ''
                @cors_preflight method OPTIONS
                @emptyorigin header !Origin

                header @emptyorigin {
                    Access-Control-Allow-Origin "*"
                    Access-Control-Allow-Methods "GET,POST,OPTIONS"
                    Access-Control-Allow-Headers "Origin,Content-Type,Accept,Authorization"
                    Access-Control-Allow-Credentials "true"
                };

                import cors http://localhost:3000
                import cors https://web.example.com
                import cors null

                handle @cors_preflight {
                    header {
                        Access-Control-Max-Age "3600"
                    }
                    respond "" 204
                }

                reverse_proxy localhost:8181
            '';
        };
    };
}

결론

쓸데없이 번거롭다.