前面一节初步学习了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字段:
apiVersion:
kind: RequestAuthentication
metadata:
name: "jwt-example"
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
jwtRules:
- issuer: "testing@;
jwksUri: ";
EOF
JWKS是一个json文件:
{
"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文件格式如下:
{
"keys": [
<jwk1>,
<jwk2>,
...
]}
使用jwx命令行工具生成JWK
那么怎么生成JWK呢?这里使用这个命令行工具。 jwx是一个用go语言开发的命令行工具,内置了对各种JWx(JWT, JWK, JWA, JWS, JWE)的支持。
使用go get安装jwx命令行工具:
go get -u -v gi
下面演示使用jwx命令行工具生成一个JWK,通过模板指定kid为myawesomekey:
jwx jwk generate --keysize 4096 --type RSA --template '{"kid":"myawesomekey"}' -o r
cat r
{
"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..."
}
从r中提取JWK公钥:
jwx jwk fmt --public-key -o r r
cat r
{
"e": "AQAB",
"kid": "myawesomekey",
"kty": "RSA",
"n": "sptztCJ....."
}
上面生成的JWK其实就是RSA公钥私钥地换了一种存储格式而已,下面演示如何将它们转换成PEM格式的公钥和私钥:
jwx jwk fmt -I json -O pem r
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCym3O0Ik5QGZ8i
......
-----END PRIVATE KEY-----
jwx jwk fmt -I json -O pem r
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5
......
-----END PUBLIC KEY-----
使用jwx命令行签发JWT Token并验证有效性
签发一个JWT Token:
jwx jws sign --key r --alg RS256 --header '{"typ":"JWT"}' -o - <<EOF
{
"iss": "testing@;,
"sub": "john007",
"iat": 1628138793,
"exp": 1629138793,
"name": "John Doe"
}
EOF
cat
eyJhbGciOiJSUzI1NiIsImtpZCI6Im15YXdlc29tZWtleSIsInR5cCI6IkpXVCJ9......
上面生成JWT Token看起来是这样的:
eyJhbGU省略部分字符CI6Ik省略部分字符省略部分字符WVvVGhNvkhXs
实际上是由下面的算法生成的:
base64url_encode(Header) + '.' + base64url_encode(Claims) + '.' + base64url_encode(Signature)
可以使用jwx命令行工具将jwt token中的header, claims(payload), signature解析出来:
jwx jws parse
Signature: "jUgIDUhHJ4A..."
Protected Headers: "eyJhbGciOiJSUzI1..."
Decoded Protected Headers: {
"alg": "RS256",
"kid": "myawesomekey",
"typ": "JWT"
}
Payload: {
"iss": "testing@;,
"sub": "john007",
"iat": 1628138793,
"exp": 1629138793,
"name": "John Doe"
}
先看一下Headers部分,包含了一些元数据,注意type和alg是必须的:
- 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,具体的签名算法如下:
RSASHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
<rsa-public-key>,
<rsa-private-key>
最后使用RSA Public Key验证JWT Token的有效性:
jwx jws verify --alg RS256 --key r
{
"iss": "testing@;,
"sub": "john007",
"iat": 1628138793,
"exp": 1629138793,
"name": "John Doe"
}
在istio认证策略中配置JWT终端用户认证
有了前面对JWT,JWK,JWKS概念的学习,下面配置istio的认证策略使用我们自己创建的JWKS。
apiVersion: ""
kind: "RequestAuthentication"
metadata:
name: "jwt-example"
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
jwtRules:
- issuer: "testing@;
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请求头。
apiVersion:
kind: AuthorizationPolicy
metadata:
name: "frontend-ingress"
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ["*"]
to:
- operation:
hosts: [";]
下面使用前面创建的jwk私钥签发一个token,确认使用新签发的token可以访问:
jwx jws sign --key r --alg RS256 --header '{"typ":"JWT"}' -o - <<EOF
{
"iss": "testing@;,
"sub": "john007",
"iat": 1628138793,
"exp": 1629138793,
"name": "John Doe"
}
EOF
TOKEN=$(cat ) && \
curl --header "Authorization: Bearer $TOKEN" -s -o /dev/null -w "%{http_code}\n"
200
curl --header "Authorization: Bearer errortoken" -s -o /dev/null -w "%{http_code}\n"
401
curl -s -o /dev/null -w "%{http_code}\n"
403
注意上面的测试中,使用签发的合法的token,可以正常访问。 使用错误的token,即RequestsAuthentication验证失败的请求,会返回401状态码。 不带token的请求,即AuthorizationPolicy验证失败的请求,会返回403状态码。
参考