Skip to content

필사 모드: SAML 2.0 딥다이브 — Assertion, Binding, Metadata 완전 정복

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

OIDC가 신규 구축의 기본값이 된 2026년에도, 엔터프라이즈 B2B SSO의 현장에서는 여전히 SAML 2.0이 공용어입니다. 고객사의 Entra ID, Okta, 또는 자체 구축 IdP와 연동하라는 요구사항을 받으면, 절반 이상의 확률로 SAML 메타데이터 XML 파일이 날아옵니다. 2005년에 표준화된 프로토콜이 20년 넘게 현역인 이유는 단순합니다. **이미 깔린 신뢰 관계의 관성**과 **브라우저만 있으면 동작하는 단순한 전제** 때문입니다.

문제는 SAML이 "설정만 하면 되는 블랙박스"로 취급되다가, 장애가 나면 아무도 내부를 모른다는 점입니다. 이 글에서는 SAML 2.0의 핵심인 Assertion, Protocol(AuthnRequest/Response), Binding, Metadata를 실제 XML 수준에서 해부하고, XML Signature Wrapping 같은 공격과 방어, 그리고 클럭 스큐 같은 운영 이슈까지 다룹니다.

SAML 2.0의 4층 구조

SAML 스펙은 네 개의 레이어로 구성됩니다. 이 구분을 알면 스펙 문서를 읽기가 훨씬 쉬워집니다.

+-----------------------------------------------------------+

| Profiles : 레이어들을 조합한 사용 시나리오 |

| (Web Browser SSO Profile, Single Logout 등) |

+-----------------------------------------------------------+

| Bindings : 메시지를 실어 나르는 전송 방법 |

| (HTTP-Redirect, HTTP-POST, Artifact, SOAP) |

+-----------------------------------------------------------+

| Protocols : 요청/응답 메시지의 형식 |

| (AuthnRequest, Response, LogoutRequest 등) |

+-----------------------------------------------------------+

| Assertions : 신원 정보의 본체 |

| (AuthnStatement, AttributeStatement 등) |

+-----------------------------------------------------------+

- **Assertion**: "이 사용자는 누구이고, 언제 어떻게 인증되었으며, 어떤 속성을 가진다"는 진술의 XML 문서.

- **Protocol**: Assertion을 요청하고 응답하는 메시지 규격.

- **Binding**: 그 메시지를 HTTP 위에 어떻게 실을지에 대한 규칙.

- **Profile**: 위 셋을 묶어 "웹 브라우저 SSO"라는 완결된 시나리오로 만든 것.

우리가 흔히 "SAML 연동"이라 부르는 것은 거의 항상 **Web Browser SSO Profile**입니다.

Assertion 해부 — 신원 진술의 XML

다음은 실제 IdP가 발급하는 Assertion의 골격입니다(서명 생략 버전).

xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"

ID="_a1b2c3d4e5f6"

Version="2.0"

IssueInstant="2026-06-12T09:30:00Z">

f9a8b7c6-1234-5678-90ab-cdef12345678

NotOnOrAfter="2026-06-12T09:35:00Z"

Recipient="https://app.example.com/saml/acs"

InResponseTo="_req-98765"/>

NotBefore="2026-06-12T09:29:00Z"

NotOnOrAfter="2026-06-12T09:35:00Z">

AuthnInstant="2026-06-12T09:30:00Z"

SessionIndex="_sess-112233">

urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport

요소별 의미와 검증 포인트

| 요소 | 의미 | SP가 검증할 것 |

| --- | --- | --- |

| Issuer | Assertion 발급자(IdP)의 entityID | 신뢰하는 IdP의 entityID와 정확히 일치하는가 |

| Subject/NameID | 사용자 식별자 | Format이 합의된 것인가 (persistent, emailAddress 등) |

| SubjectConfirmation | "이 Assertion을 제시하는 자"의 조건 | Recipient가 내 ACS URL인가, NotOnOrAfter 유효한가, InResponseTo가 내가 보낸 요청 ID인가 |

| Conditions | 유효 기간과 수신 대상 | NotBefore/NotOnOrAfter 시간 창, Audience가 내 entityID인가 |

| AuthnStatement | 언제/어떻게 인증했는가 | AuthnContextClassRef가 정책(MFA 요구 등)을 만족하는가 |

| AttributeStatement | 사용자 속성 | 매핑 규칙대로 파싱, 권한 부여 입력으로 사용 |

NameID Format은 운영에서 자주 문제가 되는 부분입니다. persistent는 서비스별로 고정된 불투명 식별자, transient는 세션마다 바뀌는 일회용, emailAddress는 사람이 읽는 이메일입니다. SP가 emailAddress를 기대하는데 IdP가 persistent를 보내면 "로그인은 되는데 계정 매칭이 안 되는" 장애가 발생합니다. 연동 초기에 반드시 합의하세요.

AuthnRequest / Response 흐름

SP-initiated SSO (표준 경로)

사용자가 SP에 먼저 접근한 경우입니다. SP가 AuthnRequest를 만들어 브라우저를 IdP로 보냅니다.

xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"

xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"

ID="_req-98765"

Version="2.0"

IssueInstant="2026-06-12T09:29:50Z"

Destination="https://idp.corp.com/saml/sso"

AssertionConsumerServiceURL="https://app.example.com/saml/acs"

ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">

Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"

AllowCreate="true"/>

urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport

IdP는 인증을 마치면 Response를 돌려줍니다. Response 안에 Assertion이 들어 있습니다.

xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"

ID="_resp-55555"

Version="2.0"

IssueInstant="2026-06-12T09:30:00Z"

Destination="https://app.example.com/saml/acs"

InResponseTo="_req-98765">

https://idp.corp.com/saml

<!-- 서명된 saml:Assertion이 여기에 위치 -->

핵심 대응 관계: AuthnRequest의 ID가 Response의 InResponseTo와, 그리고 Assertion 내부 SubjectConfirmationData의 InResponseTo와 일치해야 합니다. 이 검증을 생략하면 다른 세션용 Response를 주입하는 공격이 가능해집니다.

SP-initiated vs IdP-initiated

SP-initiated (권장) IdP-initiated

-------------------- --------------------

사용자 -> SP 접근 사용자 -> IdP 포털 접근

SP가 AuthnRequest 생성 (ID 기록) IdP가 앱 타일 클릭 시

IdP 인증 후 Response AuthnRequest 없이 바로

(InResponseTo=요청 ID) Unsolicited Response 발행

SP: InResponseTo 검증 가능 SP: InResponseTo 검증 불가

(요청 자체가 없었으므로)

CSRF/주입 방어 용이 Response 주입에 상대적으로 취약

IdP-initiated는 "회사 포털에서 앱 아이콘 클릭으로 진입"하는 UX 때문에 엔터프라이즈에서 흔히 요구되지만, InResponseTo 검증이 불가능해 보안상 열위입니다. 가능하면 IdP 포털의 앱 타일이 SP의 로그인 시작 URL을 가리키게 하여 **SP-initiated로 우회 구현**하는 것이 베스트 프랙티스입니다.

Binding — 메시지를 실어 나르는 세 가지 방법

HTTP-Redirect Binding

AuthnRequest처럼 작은 메시지에 사용합니다. 메시지를 DEFLATE 압축 → base64 → URL 인코딩하여 쿼리 스트링에 싣습니다.

GET /saml/sso?SAMLRequest=fZJNb9swDIb%2FisG7...&RelayState=abc123

&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256

&Signature=KJh8... HTTP/1.1

Host: idp.corp.com

- URL 길이 제한 때문에 큰 메시지(서명 포함 Response)에는 부적합합니다.

- 서명은 XML 내부가 아니라 **쿼리 파라미터(SigAlg, Signature)** 로 별도 전달됩니다(detached signature).

HTTP-POST Binding

Response처럼 큰 메시지에 사용합니다. IdP가 자동 제출 HTML 폼을 내려주고, 브라우저가 SP의 ACS로 POST합니다.

- base64만 적용(압축 없음), 서명은 XML 내부에 포함됩니다.

- 사실상 모든 Web SSO 연동의 Response 전달은 이 바인딩입니다.

HTTP-Artifact Binding

민감한 내용을 브라우저에 노출하지 않으려는 경우입니다. 브라우저에는 짧은 참조값(artifact)만 전달하고, SP가 back-channel(SOAP)로 IdP에서 실제 메시지를 가져옵니다.

[브라우저] [SP] [IdP]

|<-- artifact 전달 -| |

|--- artifact ----->| |

| |--- ArtifactResolve(SOAP)->|

| |<-- ArtifactResponse ------|

| | (실제 SAMLResponse) |

- 보안은 높지만 SP-IdP 간 직접 네트워크 연결이 필요하고 구현 복잡도가 높아, 실무에서는 드뭅니다.

바인딩 비교

| 항목 | HTTP-Redirect | HTTP-POST | HTTP-Artifact |

| --- | --- | --- | --- |

| 용도 | AuthnRequest, LogoutRequest | Response 전달 | 고보안 환경 |

| 인코딩 | DEFLATE + base64 + URL | base64 | artifact 참조값 |

| 서명 위치 | 쿼리 파라미터 (detached) | XML 내부 (enveloped) | XML 내부 |

| 크기 제한 | URL 길이 제한 있음 | 사실상 없음 | 해당 없음 |

| back-channel 필요 | 없음 | 없음 | 필요 (SOAP) |

| 실무 빈도 | 높음 (요청) | 매우 높음 (응답) | 낮음 |

Metadata — 신뢰 관계의 설정 파일

SAML 연동의 시작은 메타데이터 교환입니다. SP 메타데이터의 예시는 다음과 같습니다.

xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"

entityID="https://app.example.com/saml/metadata">

AuthnRequestsSigned="true"

WantAssertionsSigned="true"

protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"

Location="https://app.example.com/saml/slo"/>

index="0" isDefault="true"

Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"

Location="https://app.example.com/saml/acs"/>

IdP 메타데이터에는 SingleSignOnService 엔드포인트와 IdP의 서명 인증서가 들어 있습니다. 운영 포인트는 다음과 같습니다.

- **entityID는 식별자이지 URL이 아닙니다.** 관례상 URL 형태를 쓰지만, 문자열이 정확히 일치하는지가 전부입니다. 끝의 슬래시 하나 차이로 연동이 깨집니다.

- **인증서 만료가 SAML 장애 1위 원인입니다.** 메타데이터 안의 인증서는 TLS 인증서와 별개로 만료됩니다. 만료 90/30/7일 전 알림을 자동화하세요.

- **키 롤오버**: KeyDescriptor는 여러 개 둘 수 있습니다. 새 인증서를 메타데이터에 추가 → 상대측 갱신 확인 → 서명 키 교체 → 구 인증서 제거의 순서로 무중단 회전이 가능합니다.

- 메타데이터 자체에 서명하거나, 신뢰할 수 있는 채널(관리 콘솔, 서명된 URL)로만 교환하세요.

XML Signature와 암호화

서명 구조

SAML은 XML Signature(XMLDSig)의 enveloped signature를 사용합니다. 서명 대상의 다이제스트를 계산하고, 그 다이제스트 정보를 담은 SignedInfo를 개인키로 서명합니다.

주목할 점은 Reference의 URI가 **ID 속성 참조**라는 것입니다. "ID가 _a1b2c3d4e5f6인 요소"가 서명 대상임을 의미하며, 바로 이 간접 참조가 Signature Wrapping 공격의 빌미가 됩니다.

서명 범위는 Response 전체와 Assertion 각각에 가능합니다. **둘 다 서명하는 것이 권장**이며, 최소한 Assertion 서명은 필수입니다.

암호화 (EncryptedAssertion)

Assertion이 브라우저를 경유(front-channel)하므로, 속성에 민감 정보가 있다면 XML Encryption으로 Assertion을 SP의 공개키로 암호화할 수 있습니다. 서명이 "위조 방지"라면 암호화는 "내용 은닉"입니다. 역할이 다르므로 암호화가 서명을 대체하지 않습니다.

XML Signature Wrapping(XSW) 공격과 방어

SAML 역사상 가장 유명한 공격 계열입니다. 핵심 아이디어: **서명 검증 로직이 "서명이 유효한 요소"와 "애플리케이션이 실제로 읽는 요소"를 다르게 보도록 XML 구조를 조작**하는 것입니다.

정상 Response XSW 공격 Response

-------------- ------------------

Response Response

└─ Assertion (ID=A, 서명됨) ├─ [위조 Assertion] (ID=B, 서명 없음)

└─ Subject: alice │ └─ Subject: admin <-- 앱이 읽는 것

└─ [원본 Assertion] (ID=A, 서명 유효)

└─ Subject: alice <-- 검증기가 보는 것

서명 검증기: "ID=A 요소의 서명? 유효함" --> 통과

애플리케이션: 첫 번째 Assertion(위조)을 파싱 --> admin으로 로그인

2012년 연구에서 당시 주요 SAML 라이브러리 14개 중 다수가 이 계열 공격에 뚫렸고, 이후로도 변종이 주기적으로 발견됩니다.

방어 체크리스트

1. **직접 만들지 말 것** — 검증된 라이브러리(OpenSAML 등)의 최신 버전을 사용하고 보안 패치를 추적합니다.

2. **"서명된 바로 그 노드"만 사용** — 서명 검증에 성공한 요소의 DOM 노드에서만 데이터를 읽습니다. "문서에서 첫 번째 Assertion을 찾는" 식의 파싱은 금물입니다.

3. **스키마 검증 선행** — SAML 스키마에 어긋나는 구조(중복 Assertion, 엉뚱한 위치의 요소)를 서명 검증 전에 거부합니다.

4. **Assertion 개수 강제** — Web SSO에서 Assertion은 하나입니다. 둘 이상이면 거부합니다.

5. **Response와 Assertion 모두 서명 요구** — WantAssertionsSigned와 Response 서명 요구를 모두 켭니다.

6. **XML 파서 하드닝** — DTD와 외부 엔티티(XXE)를 비활성화합니다.

RelayState — 잊기 쉬운 조연

RelayState는 "로그인 후 어디로 돌아갈 것인가"를 운반하는 불투명 파라미터입니다. SP-initiated에서는 SP가 보낸 값이 Response와 함께 그대로 돌아오고, IdP-initiated에서는 IdP가 목적지 URL을 넣어주기도 합니다.

운영/보안 포인트:

- 스펙상 80바이트 제한이 있으므로 URL 전체가 아니라 **서버 측 상태의 키**를 넣는 것이 안전합니다.

- 돌아온 RelayState를 검증 없이 리다이렉트에 쓰면 **open redirect** 취약점이 됩니다. 화이트리스트 또는 서명/서버 측 조회로 검증하세요.

- RelayState는 서명 대상이 아니므로 변조될 수 있다는 전제로 다뤄야 합니다.

클럭 스큐 — 간헐적 장애의 단골 범인

Assertion의 NotBefore/NotOnOrAfter는 보통 발급 시각 기준 ±수 분의 짧은 창입니다. IdP와 SP의 시계가 어긋나면 이런 일이 생깁니다.

IdP 시계: 09:30:00 --> NotBefore=09:29:00 으로 발급

SP 시계: 09:28:30 --> "NotBefore가 미래" --> 거부

증상: 같은 사용자가 재시도하면 성공하기도 함 (간헐적)

특정 SP 서버(시계가 어긋난 노드)로 라우팅될 때만 실패

대응:

1. 모든 노드에 NTP/chrony를 강제하고 드리프트를 모니터링합니다.

2. SAML 라이브러리의 clock skew 허용치를 60~120초로 설정합니다(대부분 기본값 0 또는 매우 작음).

3. 장애 분석 시 "어느 노드에서 실패했는가"와 해당 노드의 시각을 함께 기록하는 로그를 갖춥니다.

검증 실패 로그에는 최소한 Issuer, InResponseTo, 거부 사유(시간 조건/Audience/서명), 그리고 서버 시각을 남기세요. "Invalid SAML response" 한 줄 로그는 운영자를 고문하는 행위입니다.

SP 구현 시 검증 체크리스트

SAML Response 수신 시 (ACS 엔드포인트):

[ ] 1. XML 파서 하드닝 상태에서 파싱 (DTD/XXE 차단)

[ ] 2. 스키마 검증

[ ] 3. Response 서명 검증 (요구하는 경우)

[ ] 4. Status가 Success인지 확인

[ ] 5. Assertion 서명 검증 — 신뢰된 IdP 인증서로

[ ] 6. 서명된 노드에서만 이후 데이터 읽기

[ ] 7. Issuer가 기대한 IdP entityID인지

[ ] 8. Conditions: NotBefore/NotOnOrAfter (skew 허용치 포함)

[ ] 9. AudienceRestriction이 내 entityID인지

[ ] 10. SubjectConfirmationData: Recipient가 내 ACS URL인지,

NotOnOrAfter 유효한지, InResponseTo가 내가 발급한 요청 ID인지

[ ] 11. Assertion ID 재사용 여부 확인 (리플레이 방지 캐시)

[ ] 12. NameID Format이 합의된 형식인지

[ ] 13. 검증 통과 후에만 앱 세션 수립

SAML이 2026년에도 살아있는 이유

1. **B2B 신뢰 관계의 관성** — 수만 개 기업의 IdP와 SaaS가 이미 SAML로 연결되어 있습니다. 동작하는 신뢰 관계를 걷어낼 비즈니스 동기가 약합니다.

2. **조달 요건** — 엔터프라이즈 SaaS 구매 체크리스트에 "SAML SSO 지원"이 여전히 명시됩니다. SSO를 프리미엄 플랜에 가두는 관행(소위 SSO tax)에 대한 비판도 그만큼 SAML이 표준 요건이라는 방증입니다.

3. **레거시 IdP 생태계** — Broadcom SiteMinder(현재 12.9) 같은 레거시 WAM 제품군이 여전히 대기업에서 가동 중이며, 이들의 표준 연동 경로가 SAML입니다. 헤더 기반 인증에서 표준 프로토콜로의 전환 과정에서도 SAML이 첫 정거장이 되는 경우가 많습니다.

4. **프로토콜 자체의 완결성** — Web SSO라는 용도에 한해서 SAML은 이미 완성된 프로토콜입니다. 변화가 없다는 것이 안정성이기도 합니다.

다만 방향은 분명합니다. 신규 기능(passkeys 연동, 토큰 교환, FAPI 등)은 모두 OIDC 진영에서 일어나고, Keycloak 같은 현대 IdP는 SAML과 OIDC를 모두 지원하므로 **IdP를 허브로 두고 레거시 SP는 SAML, 신규 앱은 OIDC**로 연결하는 하이브리드가 2026년의 표준 아키텍처입니다.

[Keycloak 26.6 / Okta / Entra ID]

| |

SAML 2.0 | | OIDC

v v

[레거시/B2B SaaS] [신규 웹/모바일/API]

마치며

SAML 2.0을 한 문장으로 요약하면 "XML Assertion을 브라우저 리다이렉트와 폼 POST로 운반하고, XML Signature로 신뢰를 보장하는 Web SSO 프로토콜"입니다. 실무에서 기억할 것은 세 가지입니다.

- **Assertion의 조건들(시간, Audience, Recipient, InResponseTo)을 하나도 빠짐없이 검증**해야 합니다. 서명 검증만으로는 부족합니다.

- **Signature Wrapping의 교훈**: 서명이 유효한 노드와 데이터를 읽는 노드가 같아야 합니다. 직접 구현하지 말고 검증된 라이브러리를 쓰세요.

- **운영 장애의 3대장**은 인증서 만료, 클럭 스큐, entityID/URL 불일치입니다. 모두 모니터링과 자동화로 예방 가능합니다.

다음 글에서는 OIDC의 내부 — Authorization Code Flow, Discovery, JWKS, 토큰 검증 — 를 같은 깊이로 다룹니다.

참고 자료

- [SAML 2.0 Core Specification (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)

- [SAML 2.0 Bindings (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf)

- [SAML 2.0 Profiles (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf)

- [SAML 2.0 Metadata (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf)

- [XML Signature Syntax and Processing (W3C)](https://www.w3.org/TR/xmldsig-core1/)

- [XML Encryption Syntax and Processing (W3C)](https://www.w3.org/TR/xmlenc-core1/)

- [On Breaking SAML: Be Whoever You Want to Be (USENIX Security 2012)](https://www.usenix.org/conference/usenixsecurity12/technical-sessions/presentation/somorovsky)

- [OWASP SAML Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html)

- [Keycloak Documentation — SAML Clients](https://www.keycloak.org/documentation)

- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [Broadcom SiteMinder Documentation](https://techdocs.broadcom.com/siteminder)

- [OpenID Connect Core 1.0 (비교 참고)](https://openid.net/specs/openid-connect-core-1_0.html)

현재 단락 (1/196)

OIDC가 신규 구축의 기본값이 된 2026년에도, 엔터프라이즈 B2B SSO의 현장에서는 여전히 SAML 2.0이 공용어입니다. 고객사의 Entra ID, Okta, 또는 자체 ...

작성 글자: 0원문 글자: 10,582작성 단락: 0/196