前面一节初步学习了istio安全管理功能中的认证策略,并使用认证策略配置了服务之间的双向TLS,使用认证策略对暴露到集群外部的http服务开启了基于JWT的终端用户认证。本节将对上节配置JWT终端用户认证时用到一些JWT相关知识做一个补充学习。

JWT即JSON Web Token,是一种用于产生访问令牌的开放标准(rfc7519)。 JWT token被设计为紧凑且安全的,特别适用于实现分布式服务的认证方案,适用于实现跨域认证方案。

JWKS(JSON Web Key Set)和JWK(JSON Web Key)

之前在使用istio配置JWT终端用户认证时,创建的认证策略中用到了jwksUri字段:

 1apiVersion: security.istio.io/v1beta1
 2kind: RequestAuthentication
 3metadata:
 4  name: "jwt-example"
 5  namespace: istio-system
 6spec:
 7  selector:
 8    matchLabels:
 9      istio: ingressgateway
10  jwtRules:
11  - issuer: "[email protected]"
12    jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.10/security/tools/jwt/samples/jwks.json"
13EOF

JWKS是一个json文件:

 1{
 2   "keys":[
 3      {
 4         "e":"AQAB",
 5         "kid":"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ",
 6         "kty":"RSA",
 7         "n":"xAE7eB6qugXyCAG3yhh7pkD..."
 8      }
 9   ]
10}

JWKS即JWK Set,也就是一组JWK。那么什么是JWK呢?JWK即JSON Web Key是JWT的秘钥,描述了一个加密密钥(公钥或私钥)的值及其各项属性。 JWKS就是一组JWK密钥。Istio就是使用JWKS提供的密钥信息对JWT token进行签名验证的。JWKS的JSON文件格式如下:

1{
2"keys": [
3  <jwk1>,
4  <jwk2>,
5  ...
6]}

使用jwx命令行工具生成JWK

那么怎么生成JWK呢?这里使用https://github.com/lestrrat-go/jwx这个命令行工具。 jwx是一个用go语言开发的命令行工具,内置了对各种JWx(JWT, JWK, JWA, JWS, JWE)的支持。

使用go get安装jwx命令行工具:

1go get -u -v github.com/lestrrat-go/jwx/cmd/jwx

下面演示使用jwx命令行工具生成一个JWK,通过模板指定kid为myawesomekey:

 1jwx jwk generate --keysize 4096 --type RSA  --template '{"kid":"myawesomekey"}' -o rsa.jwk
 2cat rsa.jwk
 3{
 4  "d": "grRsO6jTvTun5cnpBNf...",
 5  "dp": "8RUd5gEJAqxo7kMqdEFkeQu3....",
 6  "dq": "SQ2Xh8iou8Qd-THxPXKfTJDnI...",
 7  "e": "AQAB",
 8  "kid": "myawesomekey",
 9  "kty": "RSA", 
10  "n": "wIUzbQ8-WJ_u4rmDAIASBLODKlR7TMBYo...", 
11  "p": "9Ka47wKNibYmGtImrbXeLkbSngUaWXMWf2...",
12  "q": "yXNqB6DeuFWWy9eyb_Ab6VaqdeeqclmpHT...",
13  "qi": "hd4_FexqN4rpbihDRGmG_WdnE4Uwnyuqv..."
14}

从rsa.jwk中提取JWK公钥:

1jwx jwk fmt --public-key -o rsa-public.jwk rsa.jwk 
2cat rsa-public.jwk
3{
4  "e": "AQAB",
5  "kid": "myawesomekey",
6  "kty": "RSA",
7  "n": "sptztCJ....."
8}

上面生成的JWK其实就是RSA公钥私钥的换了一种存储格式而已,下面演示如何将它们转换成PEM格式的公钥和私钥:

 1jwx jwk fmt -I json -O pem rsa.jwk
 2-----BEGIN PRIVATE KEY-----
 3MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCym3O0Ik5QGZ8i
 4......
 5-----END PRIVATE KEY-----
 6
 7jwx jwk fmt -I json -O pem rsa-public.jwk
 8-----BEGIN PUBLIC KEY-----
 9MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5
10......
11-----END PUBLIC KEY-----

使用jwx命令行签发JWT Token并验证有效性

签发一个JWT Token:

 1jwx jws sign --key rsa.jwk --alg RS256 --header '{"typ":"JWT"}' -o token.txt - <<EOF
 2{
 3  "iss": "[email protected]",
 4  "sub": "john007",
 5  "iat": 1628138793,
 6  "exp": 1629138793,
 7  "name": "John Doe"
 8}
 9EOF
10
11cat token.txt
12eyJhbGciOiJSUzI1NiIsImtpZCI6Im15YXdlc29tZWtleSIsInR5cCI6IkpXVCJ9......

上面生成JWT Token看起来是这样的:

1eyJhbGU省略部分字符CI6IkpXVCJ9.ewogICJpc3MiOiA省略部分字符hbWUiOiAiSmUiCn0K.jUgIDUhHJ4Aaf省略部分字符WVvVGhNvkhXs

实际上是由下面的算法生成的:

1base64url_encode(Header) + '.' + base64url_encode(Claims) + '.' + base64url_encode(Signature)

可以使用jwx命令行工具将jwt token中的header, claims(payload), signature解析出来:

 1jwx jws parse token.txt
 2
 3Signature:                 "jUgIDUhHJ4A..."
 4Protected Headers:         "eyJhbGciOiJSUzI1..."
 5Decoded Protected Headers: {
 6                             "alg": "RS256",
 7                             "kid": "myawesomekey",
 8                             "typ": "JWT"
 9                           }
10Payload:                   {
11                             "iss": "[email protected]",
12                             "sub": "john007",
13                             "iat": 1628138793,
14                             "exp": 1629138793,
15                             "name": "John Doe"
16                           }

先看一下Headers部分,包含了一些元数据,注意typealg是必须的:

  • type: token的类型,JWT表示JWT类型的token
  • alg: 所使用的签名算法,这里是RSA256
  • kid: JWK的kid

再看一下Payload(Claims)部分,payload包含了这个token的数据信息,JWT标准规定了一些字段,另外还可以加入一些承载额外信息的字段。

  • iss: issuer,token是谁签发的
  • sub: token的主体信息,一般设置为token代表用户身份的唯一id或唯一用户名
  • exp: token过期时间,Unix时间戳格式
  • iat: token 创建时间, Unix时间戳格式
  • jti: 当前token的唯一标识

最后看一下签名Signature信息,签名是基于JSON Web Signature (JWS)标准来生成的,签名主要用于验证token是否有效,是否被篡改。 签名支持很多种算法,这里使用的是RSASHA256,具体的签名算法如下:

1RSASHA256(
2  base64UrlEncode(header) + "." +
3  base64UrlEncode(payload),
4  <rsa-public-key>,
5  <rsa-private-key>

最后使用RSA Public Key验证JWT Token的有效性:

1jwx jws verify --alg RS256 --key rsa-public.jwk token.txt
2{
3  "iss": "[email protected]",
4  "sub": "john007",
5  "iat": 1628138793,
6  "exp": 1629138793,
7  "name": "John Doe"
8}

在istio认证策略中配置JWT终端用户认证

有了前面对JWT,JWK,JWKS概念的学习,下面配置istio的认证策略使用我们自己创建的JWKS。

 1apiVersion: "security.istio.io/v1beta1"
 2kind: "RequestAuthentication"
 3metadata:
 4  name: "jwt-example"
 5  namespace: istio-system
 6spec:
 7  selector:
 8    matchLabels:
 9      istio: ingressgateway
10  jwtRules:
11  - issuer: "[email protected]"
12    forwardOriginalToken: true
13    jwks: |
14      {
15          "keys": [
16            {
17                "e": "AQAB",
18                "kid": "myawesomekey",
19                "kty": "RSA",
20                "n":    "tZdEjpwtPlRHRFqdUGX6zgCht0rT5hrbs1iXzHKJ8XIJiqgCS6HYCSR9F8ziHfW5fmhftGHmmhQxw1eXou0olIcSGHQsLjkravorvr6vMcNU4OmX48CZzVxhUMBWStbNhHOlOCMKxPnHmT4tlyuv9CwgQ0zI8_qlCpWKhlSVjTo92VRVVFHnhMAS0garwIyHmv_nHLovrdFz9fQtHPQwxvEolylnCfPANGhNJsdYbMl-7lONyB7gb_ymrc7ykt372cYEBIF419b-MIT9Sl7_hL_e4Qyw565pbipqGUDhXHSiDpTI_8Y8gK-Gev7VZu_T-BBME9VO0n00gSHIASUlw-EISnvSFrNUXCi05RN2EfH48Elc0Og9eL5OHxrXLQV6FG4pPBiR2Umx7a4rqp5X5fjbCVe0lRYL0kU7neW02n3UWItbF1oSFLL4SPQmRTMZBrECcxiq3RF51pPwBM-qAGH8KAxY3tgO_xefe8Wd3BSFZHFF_xiEGb2G0W43osFStPKQ0e7TvX2FVlP7XSpQ3Ym9FrRfKolEY7bQv2YpeCjOycFMVUR1geazEiVEJbwCrJIMQPbTI9S3_gyOBknoR34kiY52nJeLgA_jwneS4yWLVi6OESZjrgF12Kjh5Y78gwQEQD0EHJ1T9VQrdDOWtvqnK2H6HF3vcr1XyxpruQs"
21            }
22          ]
23      }      

上面的RequestAuthentication中的jwtRules中使用了jwks字段配置了我们前面创建的JWK公钥。forwardOriginalToken: true表示启用Authorization请求头转发,默认情况下,istio在完成了身份验证之后,会在转发的请求中移除Authorization请求头,为了确保后端服务获取到终端用户信息,可以设置forwardOriginalToken为true。另外注意到配置里的jwtRules是数组,可以配置多个jwtRule,实际使用中会为不同的jwt issuer配置自己的jwks,每个jwks中有多个jwk。JWK认证规则是根据JWT token payload中的iss字段匹配到istio中配置的jwks,然后根据JWT token header中的kid字段在jwks中找到对应的jwk公钥,使用找到的jwk公钥对token验证签名合法性。

上节内容我们还学习到RequestAuthentication配置的认证策略,默认情况下会忽略不带Authorization请求头的流量直接放行,需要配置授权策略AuthorizationPolicy要求必须携带Authorization请求头。

 1apiVersion: security.istio.io/v1beta1
 2kind: AuthorizationPolicy
 3metadata:
 4  name: "frontend-ingress"
 5  namespace: istio-system
 6spec:
 7  selector:
 8    matchLabels:
 9      istio: ingressgateway
10  action: DENY
11  rules:
12  - from:
13    - source:
14        notRequestPrincipals: ["*"]
15    to:
16    - operation:
17        hosts: ["httpbin.example.com"]

下面使用前面创建的jwk私钥签发一个token,确认使用新签发的token可以访问:

 1jwx jws sign --key rsa.jwk --alg RS256 --header '{"typ":"JWT"}' -o token.txt - <<EOF
 2{
 3  "iss": "[email protected]",
 4  "sub": "john007",
 5  "iat": 1628138793,
 6  "exp": 1629138793,
 7  "name": "John Doe"
 8}
 9EOF
10
11TOKEN=$(cat token.txt) && \
12curl --header "Authorization: Bearer $TOKEN" https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
13200
14
15curl --header "Authorization: Bearer errortoken" https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
16401
17
18
19curl  https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
20403

注意上面的测试中,使用签发的合法的token,可以正常访问。 使用错误的token,即RequestsAuthentication验证失败的请求,会返回401状态码。 不带token的请求,即AuthorizationPolicy验证失败的请求,会返回403状态码。

参考