카테고리 없음

Keycloak CSP frame-ancestors 로 iframe 임베드 차단된 문제 – 저장 버튼 grayed out 함정까지

임마누엘한 2026. 5. 14. 15:43

들어가며

사내 포털을 마이크로프론트엔드 (MFE) 구조로 운영 중이다. shell 앱이 여러 sub 앱을 iframe 으로 띄워서 보여주는 구조인데, 그 안에서 Keycloak 으로 보호되는 다른 앱을 iframe 으로 임베드하려고 했다. 결과는 브라우저 콘솔에 정겨운 한 줄.

Refused to display 'https://app.example.com/' in a frame because 
an ancestor violates the following Content Security Policy directive: 
"frame-ancestors 'self'".

원인은 한 번에 보이는데 해결까지 30분 이상 잡아먹었다. Keycloak Admin UI 의 silent save fail 함정 때문이었다. 검색해서 오신 분들 위해, 함정과 해결을 모두 정리한다.

핵심 키워드: Keycloak, Content-Security-Policy, frame-ancestors, X-Frame-Options, iframe 임베드, Refused to display in a frame, browserSecurityHeaders.

시스템 컨텍스트

  • 부모 페이지: https://portal.example.com/ (마이크로프론트엔드 shell)
  • iframe 으로 임베드할 대상: https://app.example.com/ (Keycloak 인증으로 보호)
  • Keycloak 버전: 24.x
  • 운영: Helm chart 로 배포, Realm 설정은 GitOps 로 관리
브라우저
  └─ portal.example.com (shell)
        └─ 
                └─ app.example.com (Keycloak 인증)

문제 상황

iframe 이 빈 화면. 콘솔에 다음 메시지.

Refused to display 'https://app.example.com/' in a frame because 
an ancestor violates the following Content Security Policy directive: 
"frame-ancestors 'self'".

이 메시지의 의미.

  • frame-ancestors = "이 페이지를 iframe 으로 띄우는 부모 페이지의 origin 화이트리스트".
  • 'self' = "자기 자신과 같은 origin 만 허용".
  •  app.example.com 자체에서 자기를 iframe 으로 띄우는 건 OK, 다른 origin (portal.example.com) 은 금지.

Keycloak default 가 frame-ancestors 'self' 라서, 다른 origin 에서 임베드하려면 Realm 의 CSP 헤더와 X-Frame-Options 를 풀어줘야 한다.

frame-ancestors vs X-Frame-Options vs frame-src 정리

헷갈리니까 한 번 정리.

헤더누가 누구를 통제하는가표준 상태

frame-ancestors (CSP) 임베드 당하는 쪽 이 "누가 나를 iframe 으로 띄울 수 있는지" 선언 신표준 (우선순위 ↑)
X-Frame-Options 동일 목적, but 단순 값 (DENY / SAMEORIGIN / ALLOW-FROM) 레거시, but 일부 브라우저는 여전히 참고
frame-src (CSP) iframe 을 띄우는 쪽 이 "어떤 origin 을 iframe 으로 띄울 수 있는지" 선언 신표준

오늘 다루는 건 첫 두 개. iframe 의 부모(shell) 에서 띄우는 쪽이 아니라 임베드 당하는 Keycloak 보호 페이지의 헤더가 막고 있는 케이스.

시도한 것들

시도 1 — Realm Security Defenses 에서 CSP 직접 수정

Keycloak Admin UI → Realm settings → Security Defenses → Headers 탭. 거기에 Content-Security-Policy 필드가 있다. 기본값:

frame-src 'self'; frame-ancestors 'self'; object-src 'none';

frame-ancestors 'self' 를 허용 origin 으로 바꿨다.

frame-src 'self'; frame-ancestors 'self' https://portal.example.com; object-src 'none';

Save 버튼 클릭. 그런데 변경이 저장되지 않는 현상.

브라우저에서 새로고침하면 default 로 돌아와 있다. 직접 Admin REST API 로 GET 해봐도 그대로.

TOKEN=$(curl -s -X POST \
  "https://kc.example.com/realms/master/protocol/openid-connect/token" \
  -d "client_id=admin-cli" -d "username=admin" -d "password=admin" \
  -d "grant_type=password" | jq -r .access_token)

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://kc.example.com/admin/realms/myrealm" | jq '.browserSecurityHeaders'

응답에 contentSecurityPolicy 가 default 값 그대로. UI 에는 분명 변경값이 보이는데 백엔드에 persist 가 안 됨.

시도 2 — UI 함정: paste 시 change detection 미발동

원인을 찾았다. 붙여넣기로만 값을 수정하면 Keycloak Admin UI 가 form change 를 감지 못 하는 케이스가 있다. Save 버튼이 grayed out (disabled) 인 채로 머무는 게 단서.

  • 눈으로 보면 grayed out 인데 클릭은 된다. 시각적 disabled 상태인데 클릭이 막혀 있지 않아서 "눌렀다" 라고 느끼지만 onClick 이 안 도는 상태.
  • 콘솔의 Network 탭을 보면 PUT 요청 자체가 나가지 않음. 즉 silent fail.

해결 트릭 (수많은 SPA 폼에서 통한다):

  1. CSP 필드를 클릭해서 커서를 안에 둠.
  2. 맨 뒤에 스페이스 한 칸 추가.
  3. 다시 백스페이스로 그 스페이스를 지움.
  4. 그 순간 React/Angular 의 change event 가 발동되어 Save 버튼이 활성화 (옅은 색 → 진한 색으로 바뀜).
  5. Save 클릭.

이 트릭은 paste-only 변경에서 dirty 감지를 놓치는 모든 폼에서 통용된다. Keycloak 외에도 일부 운영 콘솔에서 자주 만난다.

시도 3 — X-Frame-Options 도 같이 풀기

CSP 만 풀어도 일부 브라우저/구성에서는 X-Frame-Options 가 별도로 차단한다. Keycloak 의 X-Frame-Options 기본값은 SAMEORIGIN.

같은 Security Defenses 페이지에서 X-Frame-Options 를 둘 중 하나로:

  • 비워서 헤더 자체를 안 보내게 (CSP 의 frame-ancestors 가 충분히 통제).
  • ALLOW-FROM https://portal.example.com (레거시 브라우저 호환). 단, 모던 브라우저는 ALLOW-FROM 을 무시하니 신뢰하지 말 것.

가장 안전한 선택: 헤더 비워두고 CSP frame-ancestors 에 의존. 모던 브라우저 표준이고, 우선순위도 CSP 가 위.

시도 4 — 영구화 (Helm / Realm export)

여기까지 와서 Admin UI 변경이 다음 deploy 에서 사라졌다. 우리 환경은 Realm 설정이 GitOps 로 관리되는데, realm.json 의 browserSecurityHeaders 가 default 인 채여서 Helm sync 가 그걸 덮어쓴 것.

해결: realm.json 의 browserSecurityHeaders 를 함께 수정.

{
  "realm": "myrealm",
  "...": "...",
  "browserSecurityHeaders": {
    "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self' https://portal.example.com; object-src 'none';",
    "contentSecurityPolicyReportOnly": "",
    "xContentTypeOptions": "nosniff",
    "xFrameOptions": "",
    "strictTransportSecurity": "max-age=31536000; includeSubDomains",
    "xRobotsTag": "none",
    "referrerPolicy": "no-referrer"
  }
}

Helm values 에 이걸 inject 하거나 realm import 시 적용. 이로써 다음 deploy 에서도 살아남는다.

시도 5 — 검증

다시 portal 에서 iframe 으로 app 을 열면 빈 화면 대신 Keycloak 로그인 페이지(또는 이미 로그인된 경우 앱) 가 정상 표시.

# 헤더가 실제로 어떻게 나가는지 확인
curl -sI https://app.example.com/ | grep -iE 'content-security-policy|x-frame-options'

기대 출력:

content-security-policy: frame-src 'self'; frame-ancestors 'self' https://portal.example.com; object-src 'none';

X-Frame-Options 헤더가 없거나 비어 있어야 한다 (있으면 모던 브라우저가 CSP 보다 약한 정책으로 차단 가능성).

추가 함정 — 여러 origin 허용

frame-ancestors  공백으로 구분된 source list. 여러 origin 을 허용하려면:

frame-ancestors 'self' https://portal.example.com https://admin.example.com;

wildcard 도 가능하지만 보안상 권장 안 함:

frame-ancestors 'self' https://*.example.com;

production 에서는 정확한 origin 만 화이트리스트.

원인 정리

세 가지가 겹쳤다.

  1. Keycloak default CSP 가 자기 자신만 iframe 임베드 허용 (frame-ancestors 'self').
  2. Admin UI 의 paste 변경이 dirty 감지를 놓쳐서 Save 가 silently fail (Save 버튼 grayed out 이 단서).
  3. 우리 환경의 GitOps 가 realm.json 을 덮어써서, UI 변경이 다음 deploy 에 사라짐.

해결 — 최종 절차

  1. Realm settings → Security Defenses → Headers 에서 CSP / X-Frame-Options 변경.
  2. paste 후 스페이스+백스페이스 트릭으로 Save 버튼 강제 활성화.
  3. Save 후 Network 탭에서 PUT 요청이 실제로 나갔는지 확인.
  4. realm.json 의 browserSecurityHeaders 도 함께 수정하여 GitOps deploy 에서 살아남게 처리.
  5. curl -I 로 헤더가 실제로 어떻게 나가는지 검증.

디버깅 체크리스트 (같은 문제 다시 만났을 때)

  1. 브라우저 콘솔에 Refused to display ... frame-ancestors 가 보이면 100% CSP 이슈. (X-Frame-Options 였다면 메시지 문구가 다르다)
  2. Admin UI 에서 변경 후 Save 가 안 먹히는 느낌이면 DevTools Network 탭에서 PUT 이 나갔는지 확인. 1초 안에 판단 가능.
  3. curl -I 로 실제 응답 헤더 확인. 브라우저는 캐시·extension 때문에 noise 가 있다.
  4. 모던 브라우저는 X-Frame-Options 의 ALLOW-FROM 무시. CSP 의 frame-ancestors 에 의존하자.
  5. GitOps 환경이면 UI 변경 이전에 source-of-truth 파일부터 수정.

배운 점

  • CSP 수정이 안 먹히면 진짜 안 먹힌 건지 Admin UI 가 안 보낸 건지부터 분리하자. DevTools Network 탭이 1초만에 판단해준다.
  • paste-only 변경은 SPA 의 change detection 을 놓치는 경우가 많다. Keycloak 뿐 아니라 다른 운영 콘솔에서도 빈번. 스페이스+백스페이스 트릭은 반사적으로 쓸 수 있게 외워두는 게 좋다.
  • iframe 임베드 환경은 frame-ancestors / X-Frame-Options 둘 다 정리. frame-ancestors 가 신표준이고 우선순위. X-Frame-Options 는 비워두거나 일치시킨다. ALLOW-FROM 은 신뢰 안 된다.
  • 운영에서는 Admin UI 보다 realm.json / Helm values 를 single source of truth 로. UI 변경은 다음 deploy 에서 날아갈 가능성 상존. GitOps 환경이면 더더욱.
  • frame-ancestors 와 frame-src 의 방향성을 헷갈리지 말 것. 임베드 당하는 쪽이 통제하는 게 frame-ancestors. 임베드 하는 쪽이 통제하는 게 frame-src. 둘 다 CSP 이지만 역할이 정반대.

다음 단계

  • iframe 안에서 Keycloak 로그인 후 토큰 전파가 정상인지 (cross-origin cookie / SameSite 정책 확인).
  • CSP 의 frame-src 가 shell 쪽에도 정확히 설정되어 있는지 점검.
  • Content-Security-Policy-Report-Only 로 staging 에서 미리 위반 사례를 수집해두면 production 사고가 줄어든다.

참고