이 페이지에서는 서명된 IAP 헤더로 앱을 보호하는 방법에 대해 설명합니다. IAP(Identity-Aware Proxy)가 구성되면 JWT(JSON 웹 토큰)를 사용하여 앱에 대한 요청이 승인되었는지 확인합니다. 이러한 방식은 다음 종류의 위험으로부터 앱을 보호합니다.
- 실수로 사용 중지된 IAP
- 잘못 구성된 방화벽
- 프로젝트 내부에서의 액세스
앱을 올바르게 보호하려면 모든 앱 유형에 서명된 헤더를 사용해야 합니다.
또는 App Engine 표준 환경 앱이 있는 경우 Users API를 사용할 수 있습니다.
Compute Engine 및 GKE 상태 점검은 JWT 헤더를 포함하지 않으며 IAP는 상태 점검을 처리하지 않습니다. 상태 점검이 액세스 오류를 반환할 경우, Google Cloud 콘솔에서 올바르게 구성되었는지 그리고 JWT 헤더 검증에서 상태 점검 경로가 허용되는지 확인합니다. 자세한 내용은 상태 점검 예외 만들기를 참조하세요.
시작하기 전에
서명된 헤더로 앱을 보호하기 위해서는 다음이 필요합니다.
- 사용자가 연결하도록 하려는 애플리케이션
ES256
알고리즘을 지원하는 해당 언어의 제3자 JWT 라이브러리
IAP 헤더로 앱 보호
IAP JWT로 앱을 보호하기 위해서는 JWT의 헤더, 페이로드, 서명을 확인합니다. JWT는 HTTP 요청 헤더 x-goog-iap-jwt-assertion
에 있습니다. 공격자가 IAP를 우회할 경우, 공격자는 IAP 비서명 ID 헤더인 x-goog-authenticated-user-{email,id}
를 위조할 수 있습니다. IAP JWT는 보다 안전한 대안을 제공합니다.
서명된 헤더는 다른 사람이 IAP를 우회할 경우를 대비하여 보조 보안 수단을 제공합니다. IAP가 사용 설정되면 요청이 IAP 제공 인프라를 통과할 때 클라이언트가 제공한 x-goog-*
헤더가 제거됩니다.
JWT 헤더 확인
JWT 헤더가 다음 제약조건을 따르는지 확인합니다.
JWT 헤더 클레임 | ||
---|---|---|
alg |
알고리즘 | ES256 |
kid |
키 ID |
https://www.gstatic.com/iap/verify/public_key 및 https://www.gstatic.com/iap/verify/public_key-jwk 의 두 가지 형식으로 사용 가능한 IAP 키 파일에 나열된 공개 키 중 하나와 일치해야 합니다.
|
토큰의 kid
클레임에 해당하는 비공개 키로 JWT에 서명했는지 확인합니다. 이를 위해서는 먼저 다음 두 위치 중 하나에서 공개 키를 가져옵니다.
https://www.gstatic.com/iap/verify/public_key
. 이 URL에는kid
클레임을 공개 키 값에 매핑하는 JSON 딕셔너리가 포함되어 있습니다.https://www.gstatic.com/iap/verify/public_key-jwk
. 이 URL에는 JWK 형식의 IAP 공개 키가 포함됩니다.
공개 키가 준비된 다음에는 JWT 라이브러리를 사용하여 서명을 확인합니다.
JWT 페이로드 확인
JWT의 페이로드가 다음 제약조건을 따르는지 확인합니다.
JWT 페이로드 클레임 | ||
---|---|---|
exp |
만료 시간 | 미래 시간이어야 합니다. UNIX 기점을 기준으로 측정한 시간(초)입니다. 보정값은 30초가 허용됩니다. 토큰의 최대 수명은 10분 + 2 * 보정값입니다. |
iat |
발급 시간 | 과거 시간이어야 합니다. UNIX 기점을 기준으로 측정한 시간(초)입니다. 보정값은 30초가 허용됩니다. |
aud |
대상 |
다음 값이 포함된 문자열이어야 합니다.
|
iss |
발급자 |
https://cloud.google.com/iap 여야 합니다.
|
hd |
계정 도메인 |
계정이 호스팅된 도메인에 속한 경우, 계정이 연결된 도메인을 구분하기 위해 hd 클레임이 제공됩니다.
|
google |
Google 클레임 |
하나 이상의 액세스 수준이 요청에 적용될 경우 해당 이름이 google 클레임의 JSON 객체 내부의 access_levels 키 아래에 문자열 배열로 저장됩니다.
기기 정책을 지정하고 조직에 기기 데이터에 대한 액세스 권한이 있으면 |
Google Cloud 콘솔에 액세스하여 위에서 언급한 aud
문자열의 값을 가져오거나 gcloud 명령줄 도구를 사용할 수 있습니다.
Google Cloud 콘솔에서 aud
문자열 값을 얻으려면 프로젝트의 IAP(Identity-Aware Proxy) 설정으로 이동하여 부하 분산기 리소스 옆에 있는 더보기를 클릭한 후 서명된 헤더 JWT 대상을 선택합니다. 표시되는 서명된 헤더 JWT 대화상자에 선택된 리소스의 aud
클레임이 표시됩니다.
gcloud CLI
gcloud 명령줄 도구를 사용하여 aud
문자열 값을 가져오려면 프로젝트 ID를 알아야 합니다. Google Cloud 콘솔 프로젝트 정보 카드에서 프로젝트 ID를 찾은 다음 각 값에 대해 아래 지정된 명령어를 실행할 수 있습니다.
프로젝트 번호
gcloud 명령줄 도구를 사용하여 프로젝트 번호를 가져오려면 다음 명령어를 실행하세요.
gcloud projects describe PROJECT_ID
이 명령어는 다음과 비슷한 출력을 반환합니다.
createTime: '2016-10-13T16:44:28.170Z' lifecycleState: ACTIVE name: project_name parent: id: '433637338589' type: organization projectId: PROJECT_ID projectNumber: 'PROJECT_NUMBER'
서비스 ID
gcloud 명령줄 도구를 사용하여 서비스 ID를 가져오려면 다음 명령어를 실행하세요.
gcloud compute backend-services describe SERVICE_NAME --project=PROJECT_ID --global
이 명령어는 다음과 비슷한 출력을 반환합니다.
affinityCookieTtlSec: 0 backends: - balancingMode: UTILIZATION capacityScaler: 1.0 group: https://www.googleapis.com/compute/v1/projects/project_name/regions/us-central1/instanceGroups/my-group connectionDraining: drainingTimeoutSec: 0 creationTimestamp: '2017-04-03T14:01:35.687-07:00' description: '' enableCDN: false fingerprint: zaOnO4k56Cw= healthChecks: - https://www.googleapis.com/compute/v1/projects/project_name/global/httpsHealthChecks/my-hc id: 'SERVICE_ID' kind: compute#backendService loadBalancingScheme: EXTERNAL name: my-service port: 8443 portName: https protocol: HTTPS selfLink: https://www.googleapis.com/compute/v1/projects/project_name/global/backendServices/my-service sessionAffinity: NONE timeoutSec: 3610
사용자 ID 검색
위의 모든 확인이 성공한 경우 사용자 ID를 검색합니다. ID 토큰의 페이로드에는 다음 사용자 정보가 포함됩니다.
ID 토큰 페이로드 사용자 ID | ||
---|---|---|
sub |
제목 |
사용자에 대한 고유하고 안정적인 식별자입니다. x-goog-authenticated-user-id 헤더 대신 이 값을 사용하세요.
|
email |
사용자 이메일 | 사용자 이메일 주소입니다.
|
다음은 서명된 IAP 헤더로 앱을 보호하기 위한 샘플 코드입니다.
C#
Go
Java
Node.js
PHP
Python
Ruby
검증 코드 테스트
secure_token_test
쿼리 매개변수를 사용하여 앱으로 이동하면 IAP에 잘못된 JWT가 포함됩니다. 이를 사용하여 JWT 검증 로직이 다양한 실패 사례를 모두 처리할 수 있는지 확인하고 잘못된 JWT를 받을 때의 앱 동작을 확인합니다.
상태 점검 예외 만들기
앞에서 설명한 것처럼 Compute Engine 및 GKE 상태 점검은 JWT 헤더를 사용하지 않으며 IAP는 상태 점검을 처리하지 않습니다. 상태 점검을 구성할 필요가 없고, 앱이 상태 점검을 허용하도록 구성할 필요가 없습니다.
상태 점검 구성
상태 점검 경로를 아직 설정하지 않은 경우, Google Cloud 콘솔을 사용하여 상태 점검에 대한 중요하지 않은 경로를 설정하세요. 이 경로는 다른 리소스에 의해 공유되지 않도록 하세요.
- Google Cloud 콘솔 상태 점검 페이지로 이동합니다.
상태 점검 페이지로 이동 - 앱에 사용 중인 상태 점검을 클릭한 후 수정을 클릭합니다.
- 요청 경로 아래에서 중요하지 않은 경로 이름을 추가합니다. 이는 Google Cloud가 상태 점검 요청을 보낼 때 사용하는 URL 경로를 지정합니다.
생략하면 상태 점검 요청이
/
로 전송됩니다. - 저장을 클릭합니다.
JWT 검증 구성
JWT 검증 루틴을 호출하는 코드에서 상태 점검 요청 경로에 대해 200 HTTP 상태를 제공하는 조건을 추가합니다. 예를 들면 다음과 같습니다.
if HttpRequest.path_info = '/HEALTH_CHECK_REQUEST_PATH' return HttpResponse(status=200) else VALIDATION_FUNCTION
외부 ID를 위한 JWT
외부 ID와 함께 IAP를 사용하는 경우 IAP는 Google ID와 마찬가지로 인증된 모든 요청에 대해 서명된 JWT를 계속 발행합니다. 하지만 몇 가지 차이점이 있습니다.
제공업체 정보
외부 ID를 사용하는 경우 JWT 페이로드에는 gcip
라는 클레임이 포함됩니다. 이 클레임에는 이메일 및 사진 URL과 같은 사용자에 대한 정보와 추가 제공업체별 속성이 포함됩니다.
다음은 Facebook으로 로그인한 사용자를 위한 JWT 예시입니다.
"gcip": '{
"auth_time": 1553219869,
"email": "[email protected]",
"email_verified": false,
"firebase": {
"identities": {
"email": [
"[email protected]"
],
"facebook.com": [
"1234567890"
]
},
"sign_in_provider": "facebook.com",
},
"name": "Facebook User",
"picture: "https://graph.facebook.com/1234567890/picture",
"sub": "gZG0yELPypZElTmAT9I55prjHg63"
}',
email
및 sub
필드
Identity Platform에서 사용자를 인증한 경우 JWT의 email
및 sub
필드 앞에 Identity Platform 토큰 발급기관과 사용된 테넌트 ID(있는 경우)가 프리픽스로 붙습니다. 예를 들면 다음과 같습니다.
"email": "securetoken.google.com/PROJECT-ID/TENANT-ID:[email protected]", "sub": "securetoken.google.com/PROJECT-ID/TENANT-ID:gZG0yELPypZElTmAT9I55prjHg63"
sign_in_attributes
로 액세스 제어
IAM은 외부 ID와 함께 사용할 수 없지만 대신 sign_in_attributes
필드에 포함된 클레임을 사용하여 액세스를 제어할 수 있습니다. 예를 들어, SAML 제공업체를 사용하여 로그인한 사용자를 고려합니다.
{
"aud": "/projects/project_number/apps/my_project_id",
"gcip": '{
"auth_time": 1553219869,
"email": "[email protected]",
"email_verified": true,
"firebase": {
"identities": {
"email": [
"[email protected]"
],
"saml.myProvider": [
"[email protected]"
]
},
"sign_in_attributes": {
"firstname": "John",
"group": "test group",
"role": "admin",
"lastname": "Doe"
},
"sign_in_provider": "saml.myProvider",
"tenant": "my_tenant_id"
},
"sub": "gZG0yELPypZElTmAT9I55prjHg63"
}',
"email": "securetoken.google.com/my_project_id/my_tenant_id:[email protected]",
"exp": 1553220470,
"iat": 1553219870,
"iss": "https://cloud.google.com/iap",
"sub": "securetoken.google.com/my_project_id/my_tenant_id:gZG0yELPypZElTmAT9I55prjHg63"
}
유효한 역할이 있는 사용자에 대한 액세스를 제한하기 위해 아래 코드와 유사한 논리를 애플리케이션에 추가할 수 있습니다.
const gcipClaims = JSON.parse(decodedIapJwtClaims.gcip);
if (gcipClaims &&
gcipClaims.firebase &&
gcipClaims.firebase.sign_in_attributes &&
gcipClaims.firebase.sign_in_attribute.role === 'admin') {
// Allow access to admin restricted resource.
} else {
// Block access.
}
gcipClaims.gcip.firebase.sign_in_attributes
중첩 클레임을 사용하여 Identity Platform SAML 및 OIDC 제공업체의 추가 사용자 속성에 액세스할 수 있습니다.
IdP 클레임 크기 제한
사용자가 Identity Platform으로 로그인한 후 IAP에 안전하게 전달되는 스테이트리스(Stateless) Identity Platform ID 토큰에 추가 사용자 속성을 전달됩니다. 그런 후 IAP가 동일 클레임이 포함된 자체 스테이트리스(Stateless) 불투명 쿠키를 발행합니다. IAP는 쿠키 콘텐츠를 기준으로 서명된 JWT 헤더를 생성합니다.
따라서 세션이 대용량 클레임으로 시작된 경우 대부분의 브라우저에서 일반적으로 4KB 정도인 최대 허용 쿠키 크기를 초과할 수 있습니다. 그러면 로그인 작업이 실패합니다.
필요한 클레임만 IdP SAML 또는 OIDC 속성에 전달되도록 해야 합니다. 또 다른 옵션은 차단 함수를 사용하여 승인 검사에 필요하지 않은 클레임을 필터링하는 것입니다.
const gcipCloudFunctions = require('gcip-cloud-functions');
const authFunctions = new gcipCloudFunctions.Auth().functions();
// This function runs before any sign-in operation.
exports.beforeSignIn = authFunctions.beforeSignInHandler((user, context) => {
if (context.credential &&
context.credential.providerId === 'saml.my-provider') {
// Get the original claims.
const claims = context.credential.claims;
// Define this function to filter out the unnecessary claims.
claims.groups = keepNeededClaims(claims.groups);
// Return only the needed claims. The claims will be propagated to the token
// payload.
return {
sessionClaims: claims,
};
}
});