보안 문제
보안 문제 (Security Problems)
Istio 인증, 권한 부여 및 일반적인 보안 관련 문제를 해결하는 방법을 배워요. 엔드유저 인증 실패, 권한 부여 정책이 너무 제한적이거나 허용적인 문제, Istiod와 프록시가 정책을 올바르게 수용·배포·적용하는지 확인하는 방법을 다뤄요.
출처: Istio 문서
본문
엔드유저 인증이 실패하는 경우
Istio에서는 request authentication policies을 통해 엔드유저에게 인증을 활성화할 수 있어요. 다음 단계를 따라 정책 사양을 트러블슈팅하세요.
- jwksUri가 설정되어 있지 않다면 JWT 발급자가 url 형식인지, 그리고 url +
/.well-known/openid-configuration을 브라우저에서 열 수 있는지 확인하세요. 예를 들어 JWT 발급자가https://accounts.google.com이면https://accounts.google.com/.well-known/openid-configuration이 유효한 url이고 브라우저에서 열 수 있는지 확인하세요.
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: "example-3"
spec:
selector:
matchLabels:
app: httpbin
jwtRules:
- issuer: "[email protected]"
jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.31/security/tools/jwt/samples/jwks.json"
- JWT 토큰이 http 요청의 Authorization 헤더에 있다면 JWT 토큰이 유효한지(만료되지 않았는지 등) 확인하세요. JWT 토큰의 필드는 jwt.io 같은 온라인 JWT 파싱 도구로 디코딩할 수 있어요.
- istioctl proxy-config 명령으로 대상 워크로드의 Envoy 프록시 구성을 확인하세요. 위 예제 정책을 적용한 상태에서 다음 명령으로 인바운드 포트 80의 리스너 구성을 확인하세요. 정책에 지정된 issuer와 JWKS와 일치하는 설정의 envoy.filters.http.jwt_authn 필터가 보여야 해요.
$ POD=$(kubectl get pod -l app=httpbin -n foo -o jsonpath={.items..metadata.name})
$ istioctl proxy-config listener ${POD} -n foo --port 80 --type HTTP -o json
<redacted>
{
"name": "envoy.filters.http.jwt_authn",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.http.jwt_authn.v2alpha.JwtAuthentication",
"providers": {
"origins-0": {
"issuer": "[email protected]",
"localJwks": {
"inlineString": "*redacted*"
},
"payloadInMetadata": "[email protected]"
}
},
"rules": [
{
"match": {
"prefix": "/"
},
"requires": {
"requiresAny": {
"requirements": [
{
"providerName": "origins-0"
},
{
"allowMissing": {}
}
]
}
}
}
]
}
},
<redacted>
권한 부여가 너무 제한적이거나 허용적인 경우
정책 YAML 파일에 오타가 없는지 확인하세요
흔한 실수 중 하나는 YAML에서 여러 항목을 의도치 않게 지정하는 것이에요. 다음 정책을 예로 들어 보세요.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: example
namespace: foo
spec:
action: ALLOW
rules:
- to:
- operation:
paths:
- /foo
- from:
- source:
namespaces:
- foo
경로가 /foo 이고 소스 네임스페이스가 foo이면 정책이 요청을 허용할 거라고 기대할 수도 있어요. 하지만 실제로 정책은 경로가 /foo 이거나 소스 네임스페이스가 foo이면 요청을 허용하며, 이는 더 허용적이에요.
YAML 문법에서 from: 앞의 -는 목록의 새 요소를 의미해요. 이는 정책에 1개가 아닌 2개의 규칙을 만들어요. authorization policy에서 여러 규칙은 OR 의미론을 가져요.
문제를 해결하려면 여분의 -를 제거해서 경로가 /foo 이고 소스 네임스페이스가 foo인 경우에만 요청을 허용하는 단일 규칙 정책으로 만들면 더 제한적이 돼요.
TCP 포트에 HTTP 전용 필드를 사용하지 않는지 확인하세요
HTTP 전용 필드(예: host, path, headers, JWT 등)는 원시 TCP 연결에는 존재하지 않으므로 authorization policy는 더 제한적이 돼요.
ALLOW 정책의 경우 이런 필드는 절대 매칭되지 않아요. DENY 및 CUSTOM action의 경우 이 필드는 항상 매칭된 것으로 간주돼요. 최종 효과는 예상치 못한 거부를 일으킬 수 있는 더 제한적인 정책이에요.
쿠버네티스 service 정의를 확인해서 포트가 올바른 프로토콜로 적절히 이름 붙여졌는지 검증하세요. 포트에 HTTP 전용 필드를 사용한다면 포트 이름에 http- 접두사가 있는지 확인하세요.
정책이 올바른 대상에 적용되는지 확인하세요
워크로드 selector와 네임스페이스를 확인해서 올바른 대상에 적용되는지 확인하세요. istioctl x authz check POD-NAME.POD-NAMESPACE를 실행해서 적용 중인 authorization policy를 확인할 수 있어요.
정책에 지정된 action에 주의하세요
- 지정하지 않으면 정책은 기본적으로 action ALLOW를 사용해요.
- 워크로드에 여러 action(CUSTOM, ALLOW, DENY)이 동시에 적용되면 요청을 허용하려면 모든 action이 충족되어야 해요. 즉, 어떤 action이든 거부하면 요청이 거부되고, 모든 action이 허용할 때만 허용돼요.
- AUDIT action은 접근 제어를 강제하지 않으며 어떤 경우에도 요청을 거부하지 않아요.
평가 순서에 대한 자세한 내용은 authorization implicit enablement를 읽어 보세요.
Istiod가 정책을 수용하는지 확인하기
Istiod는 사용자의 authorization policy를 변환해 프록시에 배포해요. 다음 단계는 Istiod가 예상대로 작동하는지 확인하는 데 도움이 돼요.
- 다음 명령을 실행해 istiod에서 디버그 로깅을 활성화하세요.
$ istioctl admin log --level authorization:debug
- 다음 명령으로 Istiod 로그를 가져오세요. 이 정책들에 대한 디버그 출력이 생성되도록 authorization policy를 먼저 삭제한 다음 다시 적용해야 할 수도 있어요.
$ kubectl logs $(kubectl -n istio-system get pods -l app=istiod -o jsonpath='{.items[0].metadata.name}') -c discovery -n istio-system
- 출력을 확인하고 오류가 없는지 검증하세요. 예를 들어 다음과 비슷한 것을 볼 수 있어요.
2021-04-23T20:53:29.507314Z info ads Push debounce stable[31] 1: 100.981865ms since last change, 100.981653ms since last push, full=true
2021-04-23T20:53:29.507641Z info ads XDS: Pushing:2021-04-23T20:53:29Z/23 Services:15 ConnectedEndpoints:2 Version:2021-04-23T20:53:29Z/23
2021-04-23T20:53:29.507911Z debug authorization Processed authorization policy for httpbin-74fb669cc6-lpscm.foo with details:
* found 0 CUSTOM actions
2021-04-23T20:53:29.508077Z debug authorization Processed authorization policy for curl-557747455f-6dxbl.foo with details:
* found 0 CUSTOM actions
2021-04-23T20:53:29.508128Z debug authorization Processed authorization policy for httpbin-74fb669cc6-lpscm.foo with details:
* found 1 DENY actions, 0 ALLOW actions, 0 AUDIT actions
* generated config from rule ns[foo]-policy[deny-path-headers]-rule[0] on HTTP filter chain successfully
* built 1 HTTP filters for DENY action
* added 1 HTTP filters to filter chain 0
* added 1 HTTP filters to filter chain 1
2021-04-23T20:53:29.508158Z debug authorization Processed authorization policy for curl-557747455f-6dxbl.foo with details:
* found 0 DENY actions, 0 ALLOW actions, 0 AUDIT actions
2021-04-23T20:53:29.509097Z debug authorization Processed authorization policy for curl-557747455f-6dxbl.foo with details:
* found 0 CUSTOM actions
2021-04-23T20:53:29.509167Z debug authorization Processed authorization policy for curl-557747455f-6dxbl.foo with details:
* found 0 DENY actions, 0 ALLOW actions, 0 AUDIT actions
2021-04-23T20:53:29.509501Z debug authorization Processed authorization policy for httpbin-74fb669cc6-lpscm.foo with details:
* found 0 CUSTOM actions
2021-04-23T20:53:29.509652Z debug authorization Processed authorization policy for httpbin-74fb669cc6-lpscm.foo with details:
* found 1 DENY actions, 0 ALLOW actions, 0 AUDIT actions
* generated config from rule ns[foo]-policy[deny-path-headers]-rule[0] on HTTP filter chain successfully
* built 1 HTTP filters for DENY action
* added 1 HTTP filters to filter chain 0
* added 1 HTTP filters to filter chain 1
* generated config from rule ns[foo]-policy[deny-path-headers]-rule[0] on TCP filter chain successfully
* built 1 TCP filters for DENY action
* added 1 TCP filters to filter chain 2
* added 1 TCP filters to filter chain 3
* added 1 TCP filters to filter chain 4
2021-04-23T20:53:29.510903Z info ads LDS: PUSH for node:curl-557747455f-6dxbl.foo resources:18 size:85.0kB
2021-04-23T20:53:29.511487Z info ads LDS: PUSH for node:httpbin-74fb669cc6-lpscm.foo resources:18 size:86.4kB
이는 Istiod가 생성했음을 보여줘요: 워크로드 httpbin-74fb669cc6-lpscm.foo에 대한 정책 ns[foo]-policy[deny-path-headers]-rule[0]이 있는 HTTP 필터 구성과, 워크로드 httpbin-74fb669cc6-lpscm.foo에 대한 정책 ns[foo]-policy[deny-path-headers]-rule[0]이 있는 TCP 필터 구성.
Istiod가 프록시에 정책을 올바르게 배포하는지 확인하기
Istiod는 authorization policy를 프록시에 배포해요. 다음 단계는 istiod가 예상대로 작동하는지 확인하는 데 도움이 돼요.
- 다음 명령을 실행해 httpbin 워크로드의 프록시 구성 덤프를 가져오세요.
$ kubectl exec $(kubectl get pods -l app=httpbin -o jsonpath='{.items[0].metadata.name}') -c istio-proxy -- pilot-agent request GET config_dump
-
로그를 확인하고 다음을 검증하세요: 로그는 각 들어오는 요청에 대해 authorization policy를 강제하는 envoy.filters.http.rbac 필터를 포함해요. Istio는 authorization policy를 업데이트한 후 그에 맞게 필터를 업데이트해요.
-
다음 출력은 httpbin의 프록시가
/headers경로에 대한 접근을 거부하는 규칙으로 envoy.filters.http.rbac 필터를 활성화했음을 의미해요.
{
"name": "envoy.filters.http.rbac",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
"rules": {
"action": "DENY",
"policies": {
"ns[foo]-policy[deny-path-headers]-rule[0]": {
"permissions": [
{
"and_rules": {
"rules": [
{
"or_rules": {
"rules": [
{
"url_path": {
"path": {
"exact": "/headers"
}
}
}
]
}
}
]
}
}
],
"principals": [
{
"and_ids": {
"ids": [
{
"any": true
}
]
}
}
]
}
}
},
"shadow_rules_stat_prefix": "istio_dry_run_allow_"
}
},
프록시가 정책을 올바르게 강제하는지 확인하기
프록시는 결국 authorization policy를 강제해요. 다음 단계는 프록시가 예상대로 작동하는지 확인하는 데 도움이 돼요.
- 다음 명령으로 프록시에서 authorization 디버그 로깅을 켜세요.
$ istioctl proxy-config log deploy/httpbin --level "rbac:debug"
- 다음 출력이 보이는지 확인하세요.
active loggers:
... ...
rbac: debug
... ...
- httpbin 워크로드에 몇 가지 요청을 보내 로그를 생성하세요.
- 다음 명령으로 프록시 로그를 출력하세요.
$ kubectl logs $(kubectl get pods -l app=httpbin -o jsonpath='{.items[0].metadata.name}') -c istio-proxy
-
출력을 확인하고 다음을 검증하세요: 출력 로그는 요청이 허용되었는지 거부되었는지에 따라
enforced allowed또는enforced denied를 보여줘요. 사용자의 authorization policy는 요청에서 추출된 데이터를 기대해요. -
다음은
/httpbin경로의 요청에 대한 출력 예시예요.
...
2021-04-23T20:43:18.552857Z debug envoy rbac checking request: requestedServerName: outbound_.8000_._.httpbin.foo.svc.cluster.local, sourceIP: 10.44.3.13:46180, directRemoteIP: 10.44.3.13:46180, remoteIP: 10.44.3.13:46180,localAddress: 10.44.1.18:80, ssl: uriSanPeerCertificate: spiffe://cluster.local/ns/foo/sa/curl, dnsSanPeerCertificate: , subjectPeerCertificate: , headers: ':authority', 'httpbin:8000'
':path', '/headers'
':method', 'GET'
':scheme', 'http'
'user-agent', 'curl/7.76.1-DEV'
'accept', '*/*'
'x-forwarded-proto', 'http'
'x-request-id', '672c9166-738c-4865-b541-128259cc65e5'
'x-envoy-attempt-count', '1'
'x-b3-traceid', '8a124905edf4291a21df326729b264e9'
'x-b3-spanid', '21df326729b264e9'
'x-b3-sampled', '0'
'x-forwarded-client-cert', 'By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=d64cd6750a3af8685defbbe4dd8c467ebe80f6be4bfe9ca718e81cd94129fc1d;Subject="";URI=spiffe://cluster.local/ns/foo/sa/curl'
, dynamicMetadata: filter_metadata {
key: "istio_authn"
value {
fields {
key: "request.auth.principal"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
fields {
key: "source.namespace"
value {
string_value: "foo"
}
}
fields {
key: "source.principal"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
fields {
key: "source.user"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
}
}
2021-04-23T20:43:18.552910Z debug envoy rbac enforced denied, matched policy ns[foo]-policy[deny-path-headers]-rule[0]
...
로그 enforced denied, matched policy ns[foo]-policy[deny-path-headers]-rule[0]은 요청이 정책 ns[foo]-policy[deny-path-headers]-rule[0]에 의해 거부됐다는 뜻이에요.
- 다음은 dry-run 모드의 authorization policy에 대한 출력 예시예요.
...
2021-04-23T20:59:11.838468Z debug envoy rbac checking request: requestedServerName: outbound_.8000_._.httpbin.foo.svc.cluster.local, sourceIP: 10.44.3.13:49826, directRemoteIP: 10.44.3.13:49826, remoteIP: 10.44.3.13:49826,localAddress: 10.44.1.18:80, ssl: uriSanPeerCertificate: spiffe://cluster.local/ns/foo/sa/curl, dnsSanPeerCertificate: , subjectPeerCertificate: , headers: ':authority', 'httpbin:8000'
':path', '/headers'
':method', 'GET'
':scheme', 'http'
'user-agent', 'curl/7.76.1-DEV'
'accept', '*/*'
'x-forwarded-proto', 'http'
'x-request-id', 'e7b2fdb0-d2ea-4782-987c-7845939e6313'
'x-envoy-attempt-count', '1'
'x-b3-traceid', '696607fc4382b50017c1f7017054c751'
'x-b3-spanid', '17c1f7017054c751'
'x-b3-sampled', '0'
'x-forwarded-client-cert', 'By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=d64cd6750a3af8685defbbe4dd8c467ebe80f6be4bfe9ca718e81cd94129fc1d;Subject="";URI=spiffe://cluster.local/ns/foo/sa/curl'
, dynamicMetadata: filter_metadata {
key: "istio_authn"
value {
fields {
key: "request.auth.principal"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
fields {
key: "source.namespace"
value {
string_value: "foo"
}
}
fields {
key: "source.principal"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
fields {
key: "source.user"
value {
string_value: "cluster.local/ns/foo/sa/curl"
}
}
}
}