
# 호스팅 페이지와 프런트 API의 웹 보안

호스팅 로그인, 계정 포털, 이메일 페이지(인증, 비밀번호 재설정, 초대)와 이들이 호출하는 브라우저 API는 모든 요금제에서 고정된 웹 보안 기준을 따릅니다. 이러한 보호 기능은 끌 수 있는 설정이 아닙니다. 사용자가 제어할 수 있는 설정은 브라우저에서 OAuth 엔드포인트를 호출할 수 있는 웹 출처를 지정하는 것뿐입니다.

## 사이트 간 요청 위조(CSRF)

상태를 변경하는 요청은 아무것도 읽거나 변경하기 전에 확인됩니다.

- **출처 확인.** 호스팅 로그인 API 또는 인증이나 재설정 페이지에 대한 상태 변경 요청에는 환경의 로그인 호스트와 정확히 일치하는 `Origin` 헤더가 있어야 합니다. 다른 출처는 `origin_not_allowed`로 거부됩니다.
- **동의 화면.** 동의 화면의 폼 제출은 `Origin`이 페이지 자체 호스트가 아니거나, `Sec-Fetch-Site`가 있고 값이 `same-origin`이 아닌 경우 거부됩니다.
- **폼 토큰.** 각 로그인 트랜잭션과 각 이메일 폼에는 환경별 키에서 HMAC-SHA256으로 생성된 고유한 CSRF 토큰이 있습니다. 로그인 API는 이 토큰을 `Idp-Csrf-Token` 헤더로 받으며, 한 폼의 토큰은 다른 폼에서 사용할 수 없습니다. 토큰은 상수 시간 방식으로 비교됩니다.
- **쿠키.** 로그인 상태를 담는 쿠키는 `SameSite=Lax`를 사용합니다(아래 참조). 따라서 다른 사이트에서 시작된 요청에는 이 쿠키가 전송되지 않습니다.

## 교차 출처 요청(CORS)

CORS는 브라우저 애플리케이션이 호출하는 OAuth 엔드포인트에서만 응답합니다. 토큰, 해지, 푸시된 인가 요청, userinfo 엔드포인트가 여기에 해당합니다. 이는 공개 단일 페이지 애플리케이션(`spa` 유형)에만 적용됩니다.

- `allowed_web_origins`(`https://host[:port]`, 최대 20개, 와일드카드 없음)로 애플리케이션별 허용 출처를 설정합니다. [애플리케이션 생성](/docs/api/management/applications/create)을 참조하십시오.
- 브라우저의 사전 요청(preflight)에는 어떤 애플리케이션이 호출하는지가 포함되지 않으므로, 환경의 활성 공개 단일 페이지 애플리케이션이 허용한 출처의 합집합으로 응답합니다. 실제 요청은 이후 호출한 애플리케이션의 출처 목록으로 확인됩니다.
- 해당 애플리케이션의 목록에 없는 출처는 `403 origin_not_allowed`로 거부되며 `Access-Control-Allow-Origin` 헤더를 받지 않습니다.
- 허용된 출처는 정확히 그대로 반환됩니다. `*`와 `null`은 전송되지 않으며, `Access-Control-Allow-Credentials`도 전송되지 않습니다. 모든 교차 출처 응답에는 `Vary: Origin`이 포함됩니다.
- 로그인 호스트 자체에서 보낸 요청은 교차 출처가 아니므로 영향을 받지 않습니다.

## 로그인 후 리디렉션

Next.js SDK를 사용하는 경우 `returnTo`는 사용자 앱의 경로여야 합니다. 전체 URL이나 `//evil.example`과 같은 값은 무시되고 사용자는 `/`로 이동합니다. [Next.js SDK](/docs/sdk/nextjs)를 참조하십시오.

## 보안 헤더

호스팅 페이지는 모든 응답에 다음 헤더를 설정합니다.

| 헤더 | 값 |
|---|---|
| `Content-Security-Policy` | `default-src 'none'`; 스크립트는 응답마다 새로 생성된 nonce가 있는 경우에만 허용; `style-src 'self'`; `connect-src 'self'`; `frame-ancestors 'none'`; `base-uri 'none'`; `object-src 'none'`; `upgrade-insecure-requests`; 스크립트에는 Trusted Types 필요 |
| `Reporting-Endpoints` 및 `report-to` | 정책 위반은 같은 호스트의 `/u/csp-report`로 보고됨 |
| `X-Frame-Options` | `DENY`: 호스팅 페이지는 프레임에 삽입할 수 없음 |
| `X-Content-Type-Options` | `nosniff` |
| `Referrer-Policy` | `no-referrer` |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Permissions-Policy` | 카메라, 마이크, 위치, 결제, USB, 시리얼, HID, 블루투스는 비활성화됨. 패스키 요청은 페이지 자체에 대해 허용됨 |
| `Cache-Control` | `no-store` |

이 정책은 인라인 스크립트와 `unsafe-` 키워드를 허용하지 않습니다. 폼 제출은 페이지 자체 호스트로 제한됩니다. 동의 화면에서는 정책이 요청 중인 애플리케이션의 리디렉션 주소도 허용하므로, 결정 결과를 해당 애플리케이션으로 되돌릴 수 있습니다.

보고 엔드포인트는 8KiB 이하의 브라우저 위반 보고서만 받으며, IP 주소별로 요청 빈도가 제한됩니다. 보고서는 지시문별로 집계됩니다. 기록될 때 쿼리 문자열, 프래그먼트, 사용자 정보는 포함되지 않습니다.

### Strict-Transport-Security

- `keenauth.app`의 환경 호스트는 `max-age=63072000; includeSubDomains`를 전송합니다.
- [사용자 지정 도메인](/docs/guides/custom-domains)은 `includeSubDomains` 없이 `max-age=63072000`만 전송합니다. KeenAuth는 도메인의 다른 하위 도메인에 대해 결정할 수 없으므로, 이 선택은 사용자에게 남겨져 있습니다.

## 쿠키

로그인 상태 쿠키는 `__Host-` 접두사를 사용합니다. 따라서 브라우저는 HTTPS를 통해, `Path=/`로, `Domain` 속성 없이 설정된 경우에만 이 쿠키를 허용합니다. 형제 하위 도메인에서는 설정할 수 없습니다.

| 쿠키 | 플래그 | 용도 |
|---|---|---|
| `__Host-idp_session` | `Secure`, `HttpOnly`, `SameSite=Lax` | 사용자의 세션. 수명은 세션의 절대 만료 시점에 끝남 |
| `__Host-idp_device` | `Secure`, `HttpOnly`, `SameSite=Lax` | 기기 인식 |
| `__Host-idp_ltx` 및 `__Host-idp_azr_*` | `Secure`, `HttpOnly`, `SameSite=Lax` | 로그인 또는 인가 시도를 이를 시작한 브라우저에 연결 |
| `__Host-idp_fed_bind` | `Secure`, `HttpOnly`, `SameSite=None` | 엔터프라이즈 SAML 로그인을 위한 단기 바인딩. ID 공급자가 다른 사이트에서 다시 게시하므로 `Lax`를 사용하면 쿠키가 제거됨 |

쿠키 값은 페이지 스크립트에서 읽을 수 없습니다(`HttpOnly`).

## 관련 문서

- [대시보드 접근 보안](/docs/security/dashboard-access)
- [테넌트 격리](/docs/security/tenant-isolation)
- [로그인 페이지 브랜딩](/docs/guides/branding)
