k8s之 service account token
在前一篇笔记中我们验证了使用sa的token作为一种认证,向apiserver发送请求,这里简述下它的认证原理和流程。
首先得知道这种token称为JWT(json web token),可以参考官网介绍,而且这是一种RFC标准。
JWT的工作原理
JWT是服务端发给客户端的一种加密凭证(通过RSA或者密码加密),客户端访问服务端(此不一定是发布凭证的服务端)时携带上这个凭证,服务端解密此凭证,验证通过就可以允许客户端访问。
在k8s中,使用RSA私钥/公钥进行加密和验证,kube-controller-manager,使用如下参数指定私钥,对token进行签名
--service-account-private-key-file = /etc/kubernets/pki/sa.key
kube-apiserver使用如下参数指定公钥,对token进行验证
--service-account-key-file = /etc/kubernets/pki/sa.pub
a. JWT结构
JWT包含三部分,分别为: Header,Payload,Signature,之间用"."分隔,所以一般形式为xxx.yyy.zzz。
header
一般包含两部分, typ指定了类型,固定为"JWT",alg指定了签名算法, 比如HMAC SHA256 or RSA。
{
"alg": "HS256",
"typ": "JWT"
}
header需要使用Base64Url 加密后,作为JWT的第一部分
payload
定义了用户数据,有定义好的知名的字段,也可以自定义字段
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
payload需要使用Base64Url 加密后,作为JWT的第二部分
Signature
签名这一步需要四个条件:Base64Url 加密后的header,Base64Url 加密后的paload,secret(可以是密码,也可以是RSA的私钥)和header中指定的加密算法。比如使用 RSASHA256 算法计算签名的公式如下:
RSASHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
上面公式执行时,大概分为两步:
a. 使用算法 RSASHA256 对 base64UrlEncode(header) + "." +
base64UrlEncode(payload) 部分计算出一个 32 位的hash值
b. 使用secret(比如RSA的私钥)对 32 位的hash值进行签名,得出一个签名后的值
Signature
b. 生成JWT
最后将这三部分(都要经过base64加密)使用"."结合起来就是最终的token
base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + base64UrlEncode(Signature)
c. 验证JWT
服务端收到token后,根据"."将三部分解析出来,
使用base64 decode 第一和第二部分,可以得到header和payload,使用header中指定的算法计算出一个 新 hash 值,
使用base64 decode 第三部分,得到签名值Signature, 再使用RSA公钥对Signature解密得到原始 hash 值。
只要新 hash 和原始 hash一致就说明JWT是有效的。
生成/验证JWT
上面只是简述原理和流程,这里实践如何生成JWT,如何验证JWT。
有两种方法来生成/验证JWT
a. 使用 jwt.io 官方提供的图形界面
b. 使用第三方库,比如python中的jwt (pip install python-jwt 通过此命令安装)
a. jwt.io
生成JWT
在右侧的decoded下面的三个框分别填写header,payload和VERIFY SIGNATURE中的private key(对于k8s来说,私钥就是/etc/kubernetes/pkt/sa.key),最上面的Algorithm选择签名算法,这里选择rs256. 会自动在左侧的encoded框显示出生成的JWT,如下图
解密JWT
把JWT填写到左侧的encoded中,系统会自动识别出header和payload,因为他俩是base64加密的,直接解密即可,但是签名部分需要提供公钥才能进行验证,否则encoded框下面会显示红色的"Invalid Signature",只要把公钥(对于k8s来说,公钥就是/etc/kubernetes/pkt/sa.pub),填写到右侧decoded下面的VERIFY SIGNATURE框的public key处即可验证,如下图
没填写公钥时
填写正确的公钥后
image.png
b. python jwt 库
如下是python脚本,使用jwt库生成JWT,并验证JWT
root@master:~/token# cat test.py
#!/bin/env python
import jwt
#私钥内容 sa.key,一定要是完整的私钥,包括前面的"-----BEGIN"和后面的"KEY-----"
private_key = b'-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAwojpJBO8QZla+3sfdYji4aXXPesNAI9ScNOpyP4mcFLNxDOQluf7TBIQfzGC7Ynk58TXv9KeC4jO8FK2UsyjF9bKf8O3j3EhO+t8zNUD35vsqovTFyr1TvvjA8MMaH51lRIvKIQma58li4yh844lauwR4O7zAnd0RIg7ib7WbwCtkMw7RPdliy84CLsHdxSh4TZ1uWkiVb8eF5WT/28PnL5VwVHhg3A2v+JVBt4Fd9aNjanMBxIq5Tt0hlGOEmc7rCUmf/WFQz4Ddw54ZYQ+cD7Y9HwTqzCWMW14YbYNmohzn/fNpX5vXevZhX8aFjcgTYW5PX8jXAbBOcjWLtsXSwIDAQABAoIBADTpCghO+dgZvt5Beaf9KEBZW/ayVKH/WVvopfhN7+SDEQY5RC1XQUlKbIQ70jGLXOAQ8OFyhpv6hNZmmMJieEWGnSMs92MjUPe8MACCO4B5J2CnkS1u+LOX3QHr7hcJti9qd7scXlrNOWgAQxg8ZD71oFM+iof0N4JgT1lCt44O9rgmQ0lXNz1R46zB4mU4V5K0qgiX/42UMZs2N0HeRQ3Fx0uP0qGdCIL9N1DSMhvmNFHqBN6wl9bs0tBrvX32l0MNaWdJccLo33fzWE3HUlaHZttETL2p94UCAcuagIb7RT+79W960w0yFPuM1wodTcsM25uo7lglAhQoMccPTKECgYEA2dDfVj6zU0aRNsmOmMpDtXxUz0a5dJDIFu5SHmekgwiPd0rQR1WmnJRt9TUILvJsAxLIw3935DSIt1akK4lppohcmApHK6/B2TA792YF0CFMVscVA9F2QaA4hlpFUsNRDlF43wzhW31spmDfYazLc7V9OZF8ZaXdroJoU2u7xRsCgYEA5KM7C6necEXkwF1wuIIB18UPMgNEPFTJxWgkaqmQSML2SEaxp3xsaAg+RNGgfBVNlZR7OapwFnhVMI0t6Q1vMim74f4XlBh3dqB1D+He6YGpqN95ElI+Ck1s+U0/wQm+iZWn47t3UA2E9NFimiqY0JgNdV317iN9/dTxshhfiZECgYBeW+0UqK746X4pFOIQcLcqXQVEkifvRnVX8cBjaZTMKx4zmJZoAMPf2zFTY7j61YxTPIT6pDLlCpkbi44tSicZvMMYHoO8ejRpCUtBHtJv2qz+ftoswEYRof46vcqAUxq/MC5Duom6H7i8zwSWhMvSgZIRKWSRiGxjmBzn3qkGdwKBgCZnRIOxBKvXEZU+HLDhJW4Yq3S7F7sKgtmlpHhGAvY1yShat3xqacsPl2X3z/0HlwCI8Cm/dxRPIgAFtrBukT7bw7Mx+sPlWCuUyBTi245dOSIkZzGsnr8cQjGdyBeki1yQxqJ52pCXtL1qbiV3AjQHVjtgjO5zB7abDf3cGjABAoGAUQ2jfqkKKV+tW4vl1i1HUxzteVn440wHmbETHthJ7va7f+2iHR5Rr3OjE5LBZ704lp3dtgKegZd+iTg9bkXdirajIdhal+xdYUiYUmeCEHxwgbGWIsK8rsYTkbaYueXr7XtqYuc1Vfz/7Gj0GQsMMVgcwHP3gwMJMImV7GeATmk=\n-----END RSA PRIVATE KEY-----'
#完整公钥内容 sa.pub
public_key = b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwojpJBO8QZla+3sfdYji4aXXPesNAI9ScNOpyP4mcFLNxDOQluf7TBIQfzGC7Ynk58TXv9KeC4jO8FK2UsyjF9bKf8O3j3EhO+t8zNUD35vsqovTFyr1TvvjA8MMaH51lRIvKIQma58li4yh844lauwR4O7zAnd0RIg7ib7WbwCtkMw7RPdliy84CLsHdxSh4TZ1uWkiVb8eF5WT/28PnL5VwVHhg3A2v+JVBt4Fd9aNjanMBxIq5Tt0hlGOEmc7rCUmf/WFQz4Ddw54ZYQ+cD7Y9HwTqzCWMW14YbYNmohzn/fNpX5vXevZhX8aFjcgTYW5PX8jXAbBOcjWLtsXSwIDAQAB\n-----END PUBLIC KEY-----'
#payload信息
payload={"iss":"kubernetes/serviceaccount","kubernetes.io/serviceaccount/namespace":"test","kubernetes.io/serviceaccount/secret.name":"sa1-token-p5wxt","kubernetes.io/serviceaccount/service-account.name":"sa1","kubernetes.io/serviceaccount/service-account.uid":"2a457ffc-53bb-4a67-bd38-9e7fb048758e","sub":"system:serviceaccount:test:sa1"}
#签名
encoded=jwt.encode(payload, private_key, algorithm='RS256', headers={"alg":"RS256","kid":"M11kEZjXxDvNYQ44jM0l4D4lX7gz6JxeEtvgtMY-Rjc"})
print("encode value:")
print(encoded)
#验证
decoded = jwt.decode(encoded, public_key, algorithms='RS256')
print("\ndecode value:")
print(decoded)
使用python3执行脚本(为什么使用python3?原因在下面),查看结果
root@master:~/token# /usr/bin/python3 ./test.py
encode value:
b'eyJhbGciOiJSUzI1NiIsImtpZCI6Ik0xMWtFWmpYeER2TllRNDRqTTBsNEQ0bFg3Z3o2SnhlRXR2Z3RNWS1SamMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJ0ZXN0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNhMS10b2tlbi1wNXd4dCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJzYTEiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIyYTQ1N2ZmYy01M2JiLTRhNjctYmQzOC05ZTdmYjA0ODc1OGUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpzYTEifQ.dd2L5lZHhdSWYai7u3W9vuamc2q4HW65U5q4EtqhAl65yrDIoDRQz9T9gwQVLUd91j4wNj_WGo6s0CZ9bbGydnTM2HF4c3zJBZdZdALbYA7SEs8N7Z70dA_OW_N1AVGTWXHV7kY5E8rYZopfXV1fiIL_nrzy0YxEfOylz5YYuACd4duUuZ_cJuzE0WcLHLHuXWptg2AnF0jnYK5XVeESDt8hL9PG8QOu3W3T2V0sMo9e9PW38PxNuSbF35rUTX13aYDoWhxIBYyCJIEFiNnsfRGNufgdw2VBpGz3RT3TUQI4c7ZW0t29JUYxdQ9mziwIHf9RVAWypeeG3Zk4zNw5RQ'
#可看到decode后的值就是payload的内容
decode value:
{'iss': 'kubernetes/serviceaccount', 'kubernetes.io/serviceaccount/namespace': 'test', 'kubernetes.io/serviceaccount/secret.name': 'sa1-token-p5wxt', 'kubernetes.io/serviceaccount/service-account.name': 'sa1', 'kubernetes.io/serviceaccount/service-account.uid': '2a457ffc-53bb-4a67-bd38-9e7fb048758e', 'sub': 'system:serviceaccount:test:sa1'}
使用python库时有两点需要注意
a. python2中字典是无序的,即输出字典内容时不按照输入顺序。而payload内容是以字典形式作为函数jwt.encode输入的,但是在函数jwt.encode中,payload字典顺序已经变了,而计算hash值时,顺序不同生成的hash值也不同,python3保证了字典顺序,所以使用python3执行脚本。
b. header中指定了 alg 和 kid,但是python jwt库会自动添加上 type=JWT,这样的话就变成了三个字段,而使用jwt.io图形界面生成JWT时,填几个字段就用几个字段生成JWT,
结果导致同样的输入参数,python库和jwt.io生成的结果不一致。可以修改python jwt库,将 type=JWT 去掉,如下代码
/usr/lib/python3/dist-packages/jwt/api_jws.py
76 def encode(self,
77 payload, # type: Union[Dict, bytes]
78 key, # type: str
79 algorithm='HS256', # type: str
80 headers=None, # type: Optional[Dict]
81 json_encoder=None # type: Optional[Callable]
82 ):
83 segments = []
84
85 if algorithm is None:
86 algorithm = 'none'
87
88 if algorithm not in self._valid_algs:
89 pass
90
91 # Header
92 header = {'typ': self.header_typ, 'alg': algorithm}
93
94 if headers:
95 self._validate_headers(headers)
//会自动将headers更新到header中,如果下面使用header的话就会多一个字段
96 header.update(headers)
97
98 json_header = force_bytes(
99 json.dumps(
100 headers, //这里原先是使用header,替换成headers即可
101 separators=(',', ':'),
102 cls=json_encoder
103 )
104 )
下面内容是python调试内容,可忽略。
(Pdb) p signature
b"u\xdd\x8b\xe6VG\x85\xd4\x96a\xa8\xbb\xbbu\xbd\xbe\xe6\xa6sj\xb8\x1dn\xb9S\x9a\xb8\x12\xda\xa1\x02^\xb9\xca\xb0\xc8\xa04P\xcf\xd4\xfd\x83\x04\x15-G}\xd6>06?\xd6\x1a\x8e\xac\xd0&}m\xb1\xb2vt\xcc\xd8qxs|\xc9\x05\x97Yt\x02\xdb`\x0e\xd2\x12\xcf\r\xed\x9e\xf4t\x0f\xce[\xf3u\x01Q\x93Yq\xd5\xeeF9\x13\xca\xd8f\x8a_]]_\x88\x82\xff\x9e\xbc\xf2\xd1\x8cD|\xec\xa5\xcf\x96\x18\xb8\x00\x9d\xe1\xdb\x94\xb9\x9f\xdc&\xec\xc4\xd1g\x0b\x1c\xb1\xee]jm\x83`'\x17H\xe7`\xaeWU\xe1\x12\x0e\xdf!/\xd3\xc6\xf1\x03\xae\xddm\xd3\xd9],2\x8f^\xf4\xf5\xb7\xf0\xfcM\xb9&\xc5\xdf\x9a\xd4M}wi\x80\xe8Z\x1cH\x05\x8c\x82$\x81\x05\x88\xd9\xec}\x11\x8d\xb9\xf8\x1d\xc3eA\xa4l\xf7E=\xd3Q\x028s\xb6V\xd2\xdd\xbd%F1u\x0ff\xce,\x08\x1d\xffQT\x05\xb2\xa5\xe7\x86\xdd\x998\xcc\xdc9E"
(Pdb) print(signature.hex())
75dd8be6564785d49661a8bbbb75bdbee6a6736ab81d6eb9539ab812daa1025eb9cab0c8a03450cfd4fd8304152d477dd63e30363fd61a8eacd0267d6db1b27674ccd87178737cc90597597402db600ed212cf0ded9ef4740fce5bf3750151935971d5ee463913cad8668a5f5d5d5f8882ff9ebcf2d18c447ceca5cf9618b8009de1db94b99fdc26ecc4d1670b1cb1ee5d6a6d8360271748e760ae5755e1120edf212fd3c6f103aedd6dd3d95d2c328f5ef4f5b7f0fc4db926c5df9ad44d7d776980e85a1c48058c8224810588d9ec7d118db9f81dc36541a46cf7453dd351023873b656d2ddbd254631750f66ce2c081dff515405b2a5e786dd9938ccdc3945
(Pdb) n
> /usr/lib/python3/dist-packages/jwt/api_jws.py(127)encode()
-> return b'.'.join(segments)
(Pdb) segments
[b'eyJhbGciOiJSUzI1NiIsImtpZCI6Ik0xMWtFWmpYeER2TllRNDRqTTBsNEQ0bFg3Z3o2SnhlRXR2Z3RNWS1SamMifQ', b'eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJ0ZXN0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNhMS10b2tlbi1wNXd4dCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJzYTEiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIyYTQ1N2ZmYy01M2JiLTRhNjctYmQzOC05ZTdmYjA0ODc1OGUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdDpzYTEifQ', b'dd2L5lZHhdSWYai7u3W9vuamc2q4HW65U5q4EtqhAl65yrDIoDRQz9T9gwQVLUd91j4wNj_WGo6s0CZ9bbGydnTM2HF4c3zJBZdZdALbYA7SEs8N7Z70dA_OW_N1AVGTWXHV7kY5E8rYZopfXV1fiIL_nrzy0YxEfOylz5YYuACd4duUuZ_cJuzE0WcLHLHuXWptg2AnF0jnYK5XVeESDt8hL9PG8QOu3W3T2V0sMo9e9PW38PxNuSbF35rUTX13aYDoWhxIBYyCJIEFiNnsfRGNufgdw2VBpGz3RT3TUQI4c7ZW0t29JUYxdQ9mziwIHf9RVAWypeeG3Zk4zNw5RQ']
sha256生成的hash值
(Pdb) p data
b'\x9e,\x86\xd2g74\xed\xdbo\x14\xaa\x02b\xd6\x01a\xa6\xde\xfb\x82\xd0,\xe0\x14\xde\x82\xbe\xde\xed\x97'
(Pdb) len(data)
32
(Pdb) print(data.hex())
9e2a2c86d2673734eddb6f14aa0262d60161a6defb82d02ce014de82bedeed97
(Pdb) print(data)
b'\x9e,\x86\xd2g74\xed\xdbo\x14\xaa\x02b\xd6\x01a\xa6\xde\xfb\x82\xd0,\xe0\x14\xde\x82\xbe\xde\xed\x97'
/usr/lib/python3/dist-packages/jwt/api_jws.py
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)
/usr/lib/python3/dist-packages/cryptography/hazmat/backends/openssl/rsa.py(414)sign()
def sign(self, data, padding, algorithm):
data, algorithm = _calculate_digest_and_algorithm(
self._backend, data, algorithm
)
return _rsa_sig_sign(self._backend, padding, algorithm, self, data)