从服务帐户到 Google IAP 保护资源的编程访问因签名无效错误而被拒绝
Programmatic access from a service account to a Google IAP protected resource denied with invalid signature error
我最近在 GKE 集群中启用了 IAP。
- 集群版本:1.15.11-gke.11
我按照此处的说明操作:https://cloud.google.com/iap/docs/enabling-kubernetes-howto
服务配置如下:
---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
name: foo-bc-iap
namespace: foo-test
spec:
iap:
enabled: true
oauthclientCredentials:
secretName: iap-client-secret
---
apiVersion: v1
kind: Service
metadata:
name: foo-internal-service
namespace: foo-test
annotations:
cloud.google.com/backend-config: '{"ports":{"80":"foo-bc-iap"}}'
spec:
type: NodePort # To create Ingress using the service.
selector:
app: foo-test
ports:
- protocol: TCP
port: 80
targetPort: 8081
我使用的凭据是 OAuth 2.0 客户端 ID(类型:Web 应用程序)。
当我在 Kubernetes 服务上激活 IAP 时确保 IAP-protected API 端点的工作方式不同后,我编写了以下测试程序以确保可以从中给出的服务帐户访问端点JSON 文件 'account.json'。
在编写这个示例应用程序时,我参考了这个文档:https://cloud.google.com/iap/docs/authentication-howto#iap_make_request-go
- google.golang.org/api v0.23.0
- 去 1.12
func (m *myApp) testAuthz(ctx *cli.Context) error {
audience := "<The client ID of the credential mentioned above>"
serviceAccountOption := idtoken.WithCredentialsFile("account.json")
client, err := idtoken.NewClient(ctx.Context, audience, serviceAccountOption)
if err != nil {
return fmt.Errorf("idtoken.NewClient: %v", err)
}
requestBody := `{
<some JSON payload>
}`
request, err := http.NewRequest("POST", "https://my.iap.protected/endpoint",
bytes.NewBuffer([]byte(requestBody)))
if err != nil {
return fmt.Errorf("http.NewRequest: %v", err)
}
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("client.Do: %v", err)
}
defer response.Body.Close()
fmt.Printf("request header = %#v\n", response.Request.Header)
fmt.Printf("response header = %#v\n", response.Header)
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
}
fmt.Printf("%d: %s\n", response.StatusCode, string(body))
return nil
}
但是当我运行这个的时候,我只能看到下面的回复。
request header = http.Header{"Authorization":[]string{"Bearer <jwt token>"}, "Content-Type":[]string{"application/json"}, "X-Cloud-Trace-Context":[]string{"c855757f20d155da1140fad1508ae3e5/17413578722158830486;o=0"}}
response header = http.Header{"Alt-Svc":[]string{"clear"}, "Content-Length":[]string{"49"}, "Content-Type":[]string{"text/html; charset=UTF-8"}, "Date":[]string{"Wed, 06 May 2020 22:17:43 GMT"}, "X-Goog-Iap-Generated-Response":[]string{"true"}}
401: Invalid IAP credentials: JWT signature is invalid
如您所见,访问被拒绝。
所以我认为 header 中用于签署 JWT 令牌的签名可能是错误的。
但我使用 jwt.io 确保了以下内容:
- header中使用的 JWT 令牌由调用方服务帐户的私钥签名
- header中使用的JWT token可以通过调用方服务账号的public密钥进行验证
- JWT 令牌是使用 RS256 算法签名的
我还查看了令牌:
{
"alg": "RS256",
"typ": "JWT",
"kid": "<the service account's private key ID>"
}
{
"iss": "<email address of the service account>",
"aud": "",
"exp": 1588806087,
"iat": 1588802487,
"sub": "<email address of the service acocunt>"
}
没什么奇怪的。
所以我不确定这里发生了什么。 如果我禁用 IAP,端点 return 正确响应。
任何人都可以提示我做错了什么吗?
JWT 令牌 exp
属性 似乎有一个过期日期(5 月 7 日 1:01)并且您的 post 来自(5 月 7 日 3:53)你试过重新生成它吗?
[编辑:我知道这应该是一条评论。由于代表率低而作为答案发布]
我试过你的代码,我发现它不适用于 google.golang.org/api v0.23.0
,但它 可以与 google.golang.org/api v0.24.0
一起使用(最新的是写作时间)。
确实是个bug,release notes提一下:
When provided, use the TokenSource from options for NewTransport. This fixes a bug in idtoken.NewClient where the wrong TokenSource was being used for authentication.
有趣的是,0.23.0 正在发送一个使用服务帐户的私钥签名的令牌,声明如下:
{
"iss":"xx@xx.iam.gserviceaccount.com",
"aud":"",
"exp":1589596554,
"iat":1589592954,
"sub":"xx@xx.iam.gserviceaccount.com"
}
和 0.24.0 发送一个用 google 的私钥 签名的令牌 。 (在内部,先前的令牌被交换为 google 签名的令牌)
{
"aud":"xxxxxxx-xxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"azp":"xx@xx.iam.gserviceaccount.com",
"email":"xx@xx.iam.gserviceaccount.com",
"email_verified":true,
"exp":1589596508,
"iat":1589592908,
"iss":"https://accounts.google.com",
"sub":"11524xxxxxxxxxxxxxxxx"
}
正如@Dirbaio 所指出的,我认为这是 v0.23.0 特有的问题。因为我现在无法升级依赖项,所以我选择创建一个不使用 idtoken.NewClient
的新 IAP 客户端。相反,它只是使用 idtoken.NewTokenSource
来创建一个 OIDC 令牌。将令牌添加到授权 header 很容易,因此我可以解决 idtoken.NewClient
创建的客户端中的问题。
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"golang.org/x/oauth2"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
)
// IAPClient is the default HTTPS client with Morse-Code KMS integration.
type IAPClient struct {
client *http.Client
tokenSource oauth2.TokenSource
}
// NewIAPClient returns an HTTP client with TLS transport, but not doing the CA checks.
func NewIAPClient(audience string, opts ...option.ClientOption) *IAPClient {
tokenSource, err := idtoken.NewTokenSource(context.Background(), audience, opts...)
if err != nil {
panic(fmt.Errorf("cannot create a new token source: %s", err.Error()))
}
return &IAPClient{
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
tokenSource: tokenSource,
}
}
// Do sends the http request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Do(request *http.Request) (*http.Response, error) {
err := c.addAuthorizationHeader(request)
if err != nil {
return nil, fmt.Errorf("couldn't override the request with the new auth header: %s", err.Error())
}
return c.client.Do(request)
}
// Get sends the http GET request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Get(url string) (*http.Response, error) {
request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return c.Do(request)
}
// Post sends the http POST request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Post(url, contentType string, body io.Reader) (*http.Response, error) {
request, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", contentType)
return c.Do(request)
}
func (c *IAPClient) addAuthorizationHeader(request *http.Request) error {
tkn, err := c.tokenSource.Token()
if err != nil {
return fmt.Errorf("cannot create a token: %s", err.Error())
}
tkn.SetAuthHeader(request)
return nil
}
我最近在 GKE 集群中启用了 IAP。
- 集群版本:1.15.11-gke.11
我按照此处的说明操作:https://cloud.google.com/iap/docs/enabling-kubernetes-howto
服务配置如下:
---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
name: foo-bc-iap
namespace: foo-test
spec:
iap:
enabled: true
oauthclientCredentials:
secretName: iap-client-secret
---
apiVersion: v1
kind: Service
metadata:
name: foo-internal-service
namespace: foo-test
annotations:
cloud.google.com/backend-config: '{"ports":{"80":"foo-bc-iap"}}'
spec:
type: NodePort # To create Ingress using the service.
selector:
app: foo-test
ports:
- protocol: TCP
port: 80
targetPort: 8081
我使用的凭据是 OAuth 2.0 客户端 ID(类型:Web 应用程序)。
当我在 Kubernetes 服务上激活 IAP 时确保 IAP-protected API 端点的工作方式不同后,我编写了以下测试程序以确保可以从中给出的服务帐户访问端点JSON 文件 'account.json'。
在编写这个示例应用程序时,我参考了这个文档:https://cloud.google.com/iap/docs/authentication-howto#iap_make_request-go
- google.golang.org/api v0.23.0
- 去 1.12
func (m *myApp) testAuthz(ctx *cli.Context) error {
audience := "<The client ID of the credential mentioned above>"
serviceAccountOption := idtoken.WithCredentialsFile("account.json")
client, err := idtoken.NewClient(ctx.Context, audience, serviceAccountOption)
if err != nil {
return fmt.Errorf("idtoken.NewClient: %v", err)
}
requestBody := `{
<some JSON payload>
}`
request, err := http.NewRequest("POST", "https://my.iap.protected/endpoint",
bytes.NewBuffer([]byte(requestBody)))
if err != nil {
return fmt.Errorf("http.NewRequest: %v", err)
}
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("client.Do: %v", err)
}
defer response.Body.Close()
fmt.Printf("request header = %#v\n", response.Request.Header)
fmt.Printf("response header = %#v\n", response.Header)
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("ioutil.ReadAll: %v", err)
}
fmt.Printf("%d: %s\n", response.StatusCode, string(body))
return nil
}
但是当我运行这个的时候,我只能看到下面的回复。
request header = http.Header{"Authorization":[]string{"Bearer <jwt token>"}, "Content-Type":[]string{"application/json"}, "X-Cloud-Trace-Context":[]string{"c855757f20d155da1140fad1508ae3e5/17413578722158830486;o=0"}}
response header = http.Header{"Alt-Svc":[]string{"clear"}, "Content-Length":[]string{"49"}, "Content-Type":[]string{"text/html; charset=UTF-8"}, "Date":[]string{"Wed, 06 May 2020 22:17:43 GMT"}, "X-Goog-Iap-Generated-Response":[]string{"true"}}
401: Invalid IAP credentials: JWT signature is invalid
如您所见,访问被拒绝。
所以我认为 header 中用于签署 JWT 令牌的签名可能是错误的。
但我使用 jwt.io 确保了以下内容:
- header中使用的 JWT 令牌由调用方服务帐户的私钥签名
- header中使用的JWT token可以通过调用方服务账号的public密钥进行验证
- JWT 令牌是使用 RS256 算法签名的
我还查看了令牌:
{
"alg": "RS256",
"typ": "JWT",
"kid": "<the service account's private key ID>"
}
{
"iss": "<email address of the service account>",
"aud": "",
"exp": 1588806087,
"iat": 1588802487,
"sub": "<email address of the service acocunt>"
}
没什么奇怪的。
所以我不确定这里发生了什么。 如果我禁用 IAP,端点 return 正确响应。
任何人都可以提示我做错了什么吗?
JWT 令牌 exp
属性 似乎有一个过期日期(5 月 7 日 1:01)并且您的 post 来自(5 月 7 日 3:53)你试过重新生成它吗?
[编辑:我知道这应该是一条评论。由于代表率低而作为答案发布]
我试过你的代码,我发现它不适用于 google.golang.org/api v0.23.0
,但它 可以与 google.golang.org/api v0.24.0
一起使用(最新的是写作时间)。
确实是个bug,release notes提一下:
When provided, use the TokenSource from options for NewTransport. This fixes a bug in idtoken.NewClient where the wrong TokenSource was being used for authentication.
有趣的是,0.23.0 正在发送一个使用服务帐户的私钥签名的令牌,声明如下:
{
"iss":"xx@xx.iam.gserviceaccount.com",
"aud":"",
"exp":1589596554,
"iat":1589592954,
"sub":"xx@xx.iam.gserviceaccount.com"
}
和 0.24.0 发送一个用 google 的私钥 签名的令牌 。 (在内部,先前的令牌被交换为 google 签名的令牌)
{
"aud":"xxxxxxx-xxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"azp":"xx@xx.iam.gserviceaccount.com",
"email":"xx@xx.iam.gserviceaccount.com",
"email_verified":true,
"exp":1589596508,
"iat":1589592908,
"iss":"https://accounts.google.com",
"sub":"11524xxxxxxxxxxxxxxxx"
}
正如@Dirbaio 所指出的,我认为这是 v0.23.0 特有的问题。因为我现在无法升级依赖项,所以我选择创建一个不使用 idtoken.NewClient
的新 IAP 客户端。相反,它只是使用 idtoken.NewTokenSource
来创建一个 OIDC 令牌。将令牌添加到授权 header 很容易,因此我可以解决 idtoken.NewClient
创建的客户端中的问题。
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"golang.org/x/oauth2"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
)
// IAPClient is the default HTTPS client with Morse-Code KMS integration.
type IAPClient struct {
client *http.Client
tokenSource oauth2.TokenSource
}
// NewIAPClient returns an HTTP client with TLS transport, but not doing the CA checks.
func NewIAPClient(audience string, opts ...option.ClientOption) *IAPClient {
tokenSource, err := idtoken.NewTokenSource(context.Background(), audience, opts...)
if err != nil {
panic(fmt.Errorf("cannot create a new token source: %s", err.Error()))
}
return &IAPClient{
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
tokenSource: tokenSource,
}
}
// Do sends the http request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Do(request *http.Request) (*http.Response, error) {
err := c.addAuthorizationHeader(request)
if err != nil {
return nil, fmt.Errorf("couldn't override the request with the new auth header: %s", err.Error())
}
return c.client.Do(request)
}
// Get sends the http GET request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Get(url string) (*http.Response, error) {
request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return c.Do(request)
}
// Post sends the http POST request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Post(url, contentType string, body io.Reader) (*http.Response, error) {
request, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", contentType)
return c.Do(request)
}
func (c *IAPClient) addAuthorizationHeader(request *http.Request) error {
tkn, err := c.tokenSource.Token()
if err != nil {
return fmt.Errorf("cannot create a token: %s", err.Error())
}
tkn.SetAuthHeader(request)
return nil
}