美文网首页Docker&Kubernetes
k8s之 service account token

k8s之 service account token

作者: 分享放大价值 | 来源:发表于2020-09-08 22:26 被阅读0次

    在前一篇笔记中我们验证了使用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,如下图

    image.png

    解密JWT
    把JWT填写到左侧的encoded中,系统会自动识别出header和payload,因为他俩是base64加密的,直接解密即可,但是签名部分需要提供公钥才能进行验证,否则encoded框下面会显示红色的"Invalid Signature",只要把公钥(对于k8s来说,公钥就是/etc/kubernetes/pkt/sa.pub),填写到右侧decoded下面的VERIFY SIGNATURE框的public key处即可验证,如下图
    没填写公钥时

    image.png

    填写正确的公钥后


    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)

    相关文章

      网友评论

        本文标题:k8s之 service account token

        本文链接:https://www.haomeiwen.com/subject/bubesktx.html