Skip to content
isdnetworks
Go back

포트가 다른데 로그인이 풀렸다

관리 화면을 8080에 두고 서비스를 80에 두었는데 관리 화면에 로그인하면 서비스 쪽 로그인이 풀렸다.

Table of contents

Open Table of contents

증상 — 한쪽 로그인이 다른 쪽을 푸는 현상

포트가 다르니 서로 영향이 없을 것으로 보고 두 화면이 각자 PHPSESSID 를 관리한다고 생각했다.

격리하려고 포트를 나눈 것이므로 그 전제가 맞는지는 확인하지 않았고 먼저 볼 것은 쿠키가 포트별로 나뉘는가였다.

브라우저가 8080과 80을 다른 자리로 볼 것이라고 막연히 생각했다. 그 생각의 근거를 대 보라면 댈 것이 없었다.

원인 — 포트를 안 가리는 쿠키

쿠키는 포트로 나뉘지 않고 같은 호스트면 그대로 공유된다.

Set-Cookie: PHPSESSID=abc...; path=/; domain=admin.example.com

같은 호스트면 8080에서 심은 Set-Cookie 가 80으로도 간다.

두 쪽이 같은 이름을 쓰고 있었다.

session.name = PHPSESSID

관리 쪽이 새 세션을 심으면 session.name 이 같으니 서비스 쪽 식별자를 덮어쓴다.

포트로 나눴으니 격리됐다고 여긴 것이 전제부터 틀렸고 격리하려던 두 화면이 실제로는 한 자리를 공유하고 있었다.

로그인이 풀리는 것은 그 공유가 밖으로 드러난 증상일 뿐이었다. 드러나지 않는 공유가 더 있는지도 함께 봐야 했다.

세션 이름을 나눴다

먼저 이름을 갈랐다.

; 서비스
session.name = SVCSESS
; 관리
session.name = ADMSESS

SVCSESSADMSESS 가 브라우저에 나란히 살면서 서로 안 덮어쓴다.

session.name 을 갈라 덮어쓰는 증상만 사라졌고 공유 자체가 없어진 것은 아니어서 무엇이 남아 있는지를 더 봐야 했다.

증상이 사라지면 거기서 멈추기 쉽다. 이번에는 원인을 이미 알고 있었으므로 같은 원인의 다른 경로를 찾을 수 있었다.

제약 — 함께 쓰던 저장 자리

세션 저장소를 봤다.

session.save_path = "tcp://session-01:6379"

session.save_path 가 같아서 관리와 서비스가 한 저장소를 보고 있었다.

session.name 을 나눠도 식별자만 알면 다른 쪽 세션을 읽을 수 있다. 관리 세션과 서비스 세션이 같은 자리에 있는 것 자체가 위험했다.

; 서비스
session.save_path = "tcp://session-01:6379?database=1"
; 관리
session.save_path = "tcp://session-01:6379?database=2"

session.namesave_path 를 둘 다 나눠야 두 세션이 실제로 갈렸다.

쿠키 이름은 브라우저 쪽의 구분이고 저장 자리는 서버 쪽의 구분이다. 양쪽이 다 나뉘어야 격리라고 부를 수 있었다.

설정 — 좁혀 놓은 쿠키 속성

관리 쿠키가 서비스 쪽으로 나가는 것도 막았다.

session.cookie_httponly = 1
session.cookie_secure   = 1
session.cookie_path     = /
session.cookie_domain   = admin.example.com

cookie_domain 을 상위 도메인으로 두면 www 를 포함한 아래 전부로 나간다.

관리 쪽은 admin.example.com 으로 좁혔고 cookie_secure 로 평문에서는 안 나가게 했는데 관리 화면을 평문으로 열 일이 없으니 잃을 것이 없었다.

범위를 좁히는 것이 이름을 나누는 것보다 근본적인 조치였는데 이름은 같은 자리에 둘을 나란히 두는 것이고 범위는 애초에 안 나가게 하는 것이다.

이름만 나누면 관리 쿠키가 서비스 쪽으로 계속 나간다. 나가지 않게 하는 것과 나가서 안 겹치게 하는 것은 다른 수준의 조치였다.

조치 — 포트가 아니라 호스트로 나눴다

포트로 나누면 쿠키 규칙과 출처 규칙이 서로 어긋난다.

쿠키          포트를 안 가린다. 같은 호스트면 공유된다
동일 출처 정책  포트를 가린다. 8080에서 80으로 요청하면 교차 출처다

쿠키는 안 나뉘는데 Origin 은 나뉘니 같은 구성을 두고 계속 헷갈리게 된다.

서비스   www.example.com     :443
관리     admin.example.com   :443

wwwadmin 으로 나누니 쿠키도 나뉘고 Origin 판정도 함께 나뉜다.

쿠키와 Origin 이 같은 기준을 쓰게 만드는 것이 헷갈림을 없애는 방법이었다.

포트로 나누는 구성은 설정 한 줄로 끝나서 손이 덜 간다. 호스트로 나누려면 인증서와 DNS가 함께 따라오지만 그 뒤로는 헷갈릴 일이 없었다.

대신 관리 화면이 서비스 API를 부르는 자리가 교차 출처가 됐다.

$allowed = ['https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
    header("Access-Control-Allow-Origin: {$origin}");
    header('Access-Control-Allow-Credentials: true');
    header('Vary: Origin');
}

Access-Control-Allow-Origin* 로 두면 자격 증명과 같이 못 쓰므로 목록 방식이 필수였다.

Vary: Origin 을 빠뜨리면 캐시가 한 출처의 응답을 다른 출처에 그대로 준다. 설정을 넣은 뒤에는 실제 응답 헤더를 받아 확인하고 두 쪽에 번갈아 로그인해 봤다.

$ curl -sI https://admin.example.com/login | grep -i set-cookie
Set-Cookie: ADMSESS=...; path=/; domain=admin.example.com; secure; HttpOnly

$ curl -sI https://www.example.com/ | grep -i set-cookie
Set-Cookie: SVCSESS=...; path=/; secure; HttpOnly

설정이 맞는 것과 응답이 맞는 것은 확인하기 전까지 다른 문제였다.

정리


Share this post on:

Previous Post
이름을 지운다고 갈리지 않는다
Next Post
서비스가 무엇을 하는지 확인하고 껐다