前面一节初步学习了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字段:

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

JWKS是一个json文件:

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

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

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

使用jwx命令行工具生成JWK

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

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

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

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

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

从rsa.jwk中提取JWK公钥:

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

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

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

jwx jwk fmt -I json -O pem rsa-public.jwk
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5
......
-----END PUBLIC KEY-----

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

签发一个JWT Token:

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

cat token.txt
eyJhbGciOiJSUzI1NiIsImtpZCI6Im15YXdlc29tZWtleSIsInR5cCI6IkpXVCJ9......

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
jwx jws parse token.txt

Signature:                 "jUgIDUhHJ4A..."
Protected Headers:         "eyJhbGciOiJSUzI1..."
Decoded Protected Headers: {
                             "alg": "RS256",
                             "kid": "myawesomekey",
                             "typ": "JWT"
                           }
Payload:                   {
                             "iss": "[email protected]",
                             "sub": "john007",
                             "iat": 1628138793,
                             "exp": 1629138793,
                             "name": "John Doe"
                           }

先看一下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,具体的签名算法如下:

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

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

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

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

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

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

上面的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请求头。

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

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

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

TOKEN=$(cat token.txt) && \
curl --header "Authorization: Bearer $TOKEN" https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
200

curl --header "Authorization: Bearer errortoken" https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
401


curl  https://httpbin.example.com/headers -s -o /dev/null -w "%{http_code}\n"
403

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

参考