SAML
고친 사람 github-actions[bot]
SAML 은 한 곳에서 로그인한 사실을 다른 서비스에 건네주는 규칙입니다. 서비스는 비밀번호를 직접 받지 않습니다. 로그인을 맡은 쪽이 서명해 보낸 진술을 받아 그 사람을 들여보냅니다. 회사 계정 하나로 사내 여러 서비스에 들어가는 방식이 대개 이것입니다.
쉽고 빠른 이해
SAML 은 로그인을 한 곳에 몰아 두는 규칙입니다. 나머지 서비스는 그 결과만 받아 씁니다. 회사 계정으로 사내 문서 도구와 인사 시스템에 각각 로그인하지 않고 들어가는 것이 그 결과입니다.
이게 없으면 서비스마다 비밀번호를 따로 받아 보관해야 합니다. 사람이 회사를 떠날 때도 서비스 수만큼 계정을 찾아 끊어야 합니다. 하나를 놓치면 그 계정이 살아남습니다.
어떻게 도나.
- 서비스가 「이 사람이 누구인지 확인해 달라」며 브라우저를 로그인 담당 쪽으로 보냅니다
- 로그인 담당 쪽이 사람을 확인합니다. 확인했다는 진술에 서명해 브라우저 편에 들려 보냅니다
- 서비스가 그 서명을 검사하고 맞으면 자기 세션을 열어 줍니다
대가가 있습니다. 로그인 담당 쪽이 멈추면 거기 붙은 서비스 전부가 같이 못 들어갑니다. 주고받는 문서가 조금만 달라져도 서명 검사가 어긋나 멀쩡한 로그인이 거부됩니다. 양쪽 서버의 시계가 벌어져도 마찬가지입니다.
브라우저로 들어가는 사내 서비스와 사서 쓰는 서비스에 씁니다. 모바일 앱이나 서버끼리 부르는 호출에는 잘 안 씁니다.
상세
이 절은 SAML 이 누구와 누구 사이의 규칙인지부터 정합니다. 그다음 오가는 진술이 어떻게 생겼는지, 한 번의 로그인이 어떤 순서로 도는지, 받는 쪽이 그 진술을 왜 믿어도 되는지를 차례로 봅니다. 끝으로 잘 깨지는 대목과 요즘 무엇과 견주어 고르는지를 짚습니다.
SAML 은 Security Assertion Markup Language 의 줄임말입니다. 우리말로는 보안 단언 마크업 언어입니다. 이름이 말하는 대로 XML(Extensible Markup Language, 확장 가능 마크업 언어)로 적은 문서를 주고받습니다.
그 문서에 담기는 진술을 단언이라고 부릅니다. 단언은 한쪽이 「이 사람은 누구이고 이런 값을 가졌다」고 적어 서명한 진술입니다. 받는 쪽은 그 사람을 스스로 확인하지 않고 서명이 맞는지만 보고 진술을 받아들입니다.
한 번 로그인해 여러 서비스를 쓰는 싱글 사인온이 이 믿음 위에 섭니다. 로그인은 한 곳에서만 일어납니다. 나머지 서비스는 「확인했다」는 남의 진술을 근거로 문을 엽니다.
신원 공급자와 서비스 공급자
주고받는 상대는 신원 공급자와 서비스 공급자 둘입니다. 앞에서 「로그인을 맡은 쪽」이라 부른 것이 신원 공급자입니다. 그 사이에서 문서를 나르는 브라우저가 하나 더 낍니다.
| 누구 | 하는 일 |
|---|---|
| 신원 공급자 | 사람을 확인하고 단언을 발급합니다 |
| 서비스 공급자 | 단언을 받아 자기 세션을 열어 줍니다 |
| 브라우저 | 두 쪽 사이에서 문서를 나릅니다 |
신원 공급자(Identity Provider)는 직원 계정 목록과 비밀번호를 들고 있는 쪽입니다. 회사 로그인 서버가 대개 여기입니다.
서비스 공급자(Service Provider)는 사람이 쓰려는 문서 도구나 인사 시스템입니다. 비밀번호를 받지도 보관하지도 않습니다. 건네받은 단언만 보고 사람을 들여보냅니다.
표의 셋째 줄이 이 규칙의 성격을 정합니다. 두 서버는 서로 직접 연결하지 않습니다. 사람의 브라우저가 문서를 들고 양쪽을 오갑니다. 그래서 사내망 안쪽에 있어 밖에서 못 닿는 신원 공급자와도 붙일 수 있습니다.
셋을 나란히 놓으면 두 서버 사이에 선이 없습니다.
flowchart TD
SP["서비스 공급자"]
BR["브라우저"]
subgraph NET["사내망"]
IDP["신원 공급자"]
end
SP --- BR
BR --- IDP
단언의 뼈대
단언은 요소를 겹겹이 담은 문서입니다. 값을 걷어내고 이름만 남기면 뼈대가 이렇습니다.
<Assertion>
<Issuer/> <!-- 누가 냈나 -->
<Subject/> <!-- 누구인가 -->
<Conditions/> <!-- 언제까지 · 누구에게 -->
<AuthnStatement/> <!-- 확인 방법 -->
<AttributeStatement/> <!-- 부서·역할 -->
<Signature/> <!-- 위조 방지 -->
</Assertion>
여섯 줄 가운데 앞의 둘이 「누가 누구에 대해 말하나」를 정합니다. Issuer 는 이 단언을 발급한 신원 공급자의 이름입니다. Subject 는 이야기의 대상이 되는 사람입니다.
가운데 셋은 그 진술의 조건과 내용입니다. Conditions 는 언제부터 언제까지 유효한지를 적습니다. 이 단언을 받을 서비스의 이름도 여기 들어갑니다. 그래서 다른 서비스로 가져가 내밀면 맞지 않습니다.
AuthnStatement 는 비밀번호로 확인했는지 다른 수단을 썼는지를 적습니다. AttributeStatement 에는 부서나 역할처럼 받는 쪽이 쓸 속성이 들어갑니다.
마지막 줄이 나머지 전부를 지킵니다. Signature 는 신원 공급자만 가진 개인 키로 찍은 전자 서명입니다. 이것이 없으면 누구나 자기에게 유리한 단언을 지어내 보낼 수 있습니다.
뼈대는 이 여섯입니다. 실제 문서에는 이 단언 하나만 가리키는 고유 식별자와 여러 부속 값이 더 붙어 훨씬 길어집니다.
한 번의 로그인이 도는 순서
사람이 서비스 주소를 처음 열었을 때부터 문서를 보기까지를 따라가 봅니다.
sequenceDiagram
participant 브라우저
participant 서비스 as 서비스 공급자
participant 신원 as 신원 공급자
브라우저->>서비스: 문서를 열겠다
서비스-->>브라우저: 신원 공급자에게 확인받아 와라
브라우저->>신원: 확인 요청을 전한다
Note over 신원: 이미 로그인해 있으면 다시 안 묻는다
신원-->>브라우저: 서명한 단언을 들려 보낸다
브라우저->>서비스: 단언을 전한다
서비스-->>브라우저: 서명을 확인하고 세션을 연다
신원 공급자가 하는 일은 그 사람이 이미 로그인해 있느냐에 따라 갈립니다. 로그인해 있으면 아무것도 묻지 않습니다. 아니면 로그인 화면을 띄워 비밀번호를 받습니다.
두 서버가 직접 연결하지 않으므로 확인 요청도 단언도 브라우저에 실려 건너갑니다. 짧은 것은 주소 뒤에 붙여 보냅니다. 긴 것은 저절로 눌리는 폼에 담아 넘깁니다.
서명 검사까지 끝나면 서비스 공급자가 자기 세션을 엽니다. 그 뒤로는 SAML 이 끼어들지 않고 평소처럼 쿠키로 세션이 이어집니다.
시작이 반대인 갈래도 있습니다. 사람이 회사 포털에서 서비스 아이콘을 눌러 들어가면 확인 요청 없이 단언부터 날아갑니다.
sequenceDiagram
participant 브라우저
participant 신원 as 신원 공급자
participant 서비스 as 서비스 공급자
브라우저->>신원: 포털에서 아이콘을 누른다
신원-->>브라우저: 서명한 단언을 들려 보낸다
브라우저->>서비스: 단언을 전한다
앞 그림의 첫 두 화살표가 여기에는 없습니다. 서비스 공급자가 확인 요청을 만들어 보내는 단계가 통째로 빠집니다.
신뢰를 미리 나눠 갖는 메타데이터
받는 쪽이 아무 단언이나 믿으면 남이 지어낸 진술로도 문이 열립니다. 그래서 두 쪽은 붙이기 전에 서로를 먼저 등록합니다.
등록할 때 나눠 갖는 것이 SAML 메타데이터 문서입니다. 여기에는 상대의 이름, 문서를 받을 주소, 서명을 검사할 때 쓸 공개 키가 인증서 형태로 담깁니다. 이 교환을 마쳐야 서비스 공급자가 「이 서명은 우리가 아는 그 신원 공급자의 것」이라고 판정할 수 있습니다.
미리 나눠 가진 공개 키가 로그인 때의 서명 검사를 떠받칩니다.
flowchart TD
subgraph PRE["붙이기 전"]
M["신원 공급자 메타데이터"] --> K["서비스 공급자가 공개 키를 보관"]
end
subgraph RUN["로그인 때"]
S["신원 공급자가 개인 키로 서명"] --> V["서비스 공급자가 서명을 검사"]
end
K --> V
그래서 붙이는 작업의 절반은 코드가 아니라 이 문서를 주고받는 일입니다. 키를 바꾸면 양쪽 문서를 같이 갱신해야 합니다. 한쪽만 바꾸면 그 순간부터 모든 로그인이 서명 검사에서 막힙니다.
잘 깨지는 대목 셋
깨지는 대목은 대개 셋으로 모입니다. 시간, 서명을 검사한 범위, 그리고 단언을 다시 쓰는 것입니다.
시간이 첫째입니다. 단언에는 유효 구간이 적혀 있어서, 두 서버의 시계가 몇 분만 벌어져도 받는 쪽이 「아직 이르다」거나 「이미 지났다」며 거부합니다. 로그인이 갑자기 전부 막혔는데 설정을 안 건드렸다면 시계부터 봅니다.
서명을 검사한 범위가 둘째입니다. 문서에서 서명이 덮은 덩이와 프로그램이 실제로 읽어 쓴 덩이가 다르면, 공격자가 서명된 덩이는 그대로 두고 옆에 가짜 덩이를 끼워 넣을 수 있습니다. 이 수법을 XML 서명 래핑이라고 부릅니다.
flowchart TD
subgraph SIG["서명이 덮은 범위"]
ORIG["원본 단언"]
end
FAKE["끼워 넣은 가짜 단언"]
CHECK["서명 검사"] --> ORIG
READ["프로그램이 읽는 곳"] --> FAKE
서명 검사는 원본 단언만 보므로 그대로 통과합니다. 정작 사람을 들여보낼 때 읽는 것은 옆에 끼워 넣은 가짜 단언입니다.
셋째는 같은 단언을 두 번 쓰는 것입니다. 가로챈 단언을 유효 구간 안에 다시 보내면 그대로 통과하므로, 받는 쪽은 한 번 쓴 단언의 식별자를 기억해 두고 두 번째를 버립니다(재전송 공격). 단언에 「누구에게 준 것인지」가 적혀 있어 다른 서비스로 가져가 쓰는 것도 막습니다.
쓸 때와 안 쓸 때
직원 계정을 이미 한 곳에서 관리하는 회사에 잘 맞습니다. 사내 서비스와 사서 쓰는 서비스에 같은 계정으로 들어가게 할 때입니다. 기업 고객에게 파는 서비스는 이 방식으로 붙여 달라는 요구를 자주 받습니다.
브라우저 밖에서는 잘 안 맞습니다. 리다이렉트와 폼 전송을 전제로 짜여 있어서 모바일 앱이나 명령줄 도구, 서버끼리 부르는 호출에 얹기 번거롭습니다. 문서 하나가 길어서 주소창에 싣기도 빠듯합니다.
그래서 새로 만드는 곳은 대개 OpenID Connect를 고릅니다. 같은 일을 하되 주고받는 것이 JWT(JSON Web Token, JSON 웹 토큰)입니다. 훨씬 짧아 앱과 서버 호출에 싣기 쉽습니다. 사내 서비스는 SAML 로, 바깥 앱은 OpenID Connect 로 두 갈래를 다 받는 신원 공급자가 흔합니다.
관련 항목
SAML 이 주고받는 메시지와 참여자
단언 · 신원 공급자 · 서비스 공급자 · SAML 메타데이터 · 주체 · 속성 · 클레임 · 싱글 사인온
SAML 이 올라타는 바탕 기술
XML · XML 서명 · XML 정규화 · 전자 서명 · 인증서 · 공개 키 암호 · Base64 · TLS · 쿠키 · 세션
같은 일을 두고 겨루는 신원 연동 규격
OpenID Connect · OAuth 2.0 · JWT · WS-Federation · CAS · Kerberos · LDAP
SAML 이 속하는 상위 분류
인증 · 인가 · 인증과 인가 · 페더레이션 · 접근 제어 · 속성 기반 접근 제어 · 신원 관리
SAML 을 붙일 때 터지는 사고
XML 서명 래핑 · 재전송 공격 · 시계 어긋남 · 세션 고정 · 권한 상승 · 중간자 공격
SAML 을 구현한 제품
Keycloak · Shibboleth · SimpleSAMLphp · Okta · Auth0 · Active Directory 페더레이션 서비스
다른 이름: Security Assertion Markup Language · 보안 단언 마크업 언어 · SAML 2.0