J'ai récemment activé IAP dans le cluster GKE.
J'ai suivi les instructions ici: https://cloud.google.com/iap/docs/enabling-kubernetes-howto
La configuration du service est la suivante:
---
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
Les informations d'identification que j'ai utilisées étaient OAuth 2.0 ID client (Type: Application Web).
Après m'être assuré que le point de terminaison d'API protégé par IAP fonctionne différemment lorsque j'active IAP sur le service Kubernetes, j'ai écrit le programme de test suivant pour m'assurer que le point de terminaison est accessible à partir du compte de service indiqué dans le fichier JSON 'account.json'.
En écrivant cet exemple d'application, j'ai consulté ce document: https://cloud.google.com/iap/docs/authentication-howto#iap_make_request-go
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
}
Mais quand je lance ceci, je ne pouvais voir que la réponse suivante.
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
Comme vous pouvez le voir ici, l'accès a été refusé .
J'ai donc pensé que la signature utilisée pour signer le jeton JWT dans l'en-tête pouvait être fausse.
Mais je me suis assuré ce qui suit en utilisant jwt.io:
Et j'ai aussi regardé dans le jeton:
{
"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>"
}
Rien de bien étrange.
Je ne suis donc pas sûr de ce qui se passe ici. Si je désactive IAP, le point final renvoie la bonne réponse .
Quelqu'un peut-il me donner une idée de ce que je fais mal?
Comme @Dirbaio l'a souligné, je pense que c'est un problème spécifique à la v0.23.0. Comme je ne peux pas mettre à niveau la dépendance pour le moment, j'ai choisi de créer un nouveau client IAP qui n'utilise pas idtoken.NewClient
. Au lieu de cela, il utilise simplement idtoken.NewTokenSource
pour créer un jeton OIDC. L'ajout du jeton à l'en-tête d'autorisation est facile afin que je puisse contourner le problème dans le client créé par 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
}