GCP의 서비스 간 인증을 위해 Go 사용

34812 단어 GoGCPCloudRuntech

개시하다


클라우드 런은 편하죠.컨테이너를 조정하기만 하면 마음대로 축소할 수 있고, 사용하지 않으면 제로 실례에서 할 수 있다.이러한 처리의 편리함 외에'좋다'와 개인의 기능 중 하나는 계정 수준의 접근 제어이다.
Authentication을 "인증되지 않은 라이센스"로 설정하면 누구나 사용할 수 있는 공개 API 상태가 되며, ACL을 설정하면 서비스 계정이나 사용자 계정 단위로 실행되는 사용자나 애플리케이션을 제한할 수 있습니다.이런 안전성은 응용 프로그램 없이도 쉽게 만들 수 있다.
이번에는 어떻게 아이덴티티톡을 꺼냅니까?"인증 방법", "ID 영패를 얻는 방법", 나는 겸사겸사 필기를 하고 싶다.
또 전반부는 조사 결과가 필요 없기 때문에 결론을 알고 싶은 사람은 갈 수 있다아랫부분.

TL;DR

  • ADC 또는 JWS 라이브러리에서 Identity Token을 추출하는 방법
  • 원래 idtoken.New Client 실행
  • 문서 자세히 읽기
  • 서비스 간 인증 개요


    아무튼 서비스 간 인증부터 살짝 확인해 볼게요.공식 수첩이 여기 있습니다.
    https://cloud.google.com/run/docs/authenticating/service-to-service?hl=ja
    기본적으로 OAuth ID 토큰을 통해 인증됩니다.따라서 어떤 인증 정보도 어기지 않고 던지면 다음과 같은 오류가 발생할 수 있다.
    curl -i https://target-xxx.a.run.app/healthcheck
    HTTP/2 403
    date: Thu, 17 Jun 2021 14:48:11 GMT
    content-type: text/html; charset=UTF-8
    server: Google Frontend
    content-length: 306
    alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    
    <html><head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>403 Forbidden</title>
    </head>
    <body text=#000000 bgcolor=#ffffff>
    <h1>Error: Forbidden</h1>
    <h2>Your client does not have permission to get URL <code>/healthcheck</code> from this server.</h2>
    <h2></h2>
    </body></html>
    
    재미있는 것은 오류에 대한 응답이 Google Fronted라는 것입니다.이때는 앱에도 요청하지 않았다는 것이다.그래서 일지를 볼 때 주의하세요.이렇게 슬라이드카처럼 다양한 일을 하는 것이 좋다.
    그렇다면 어떻게 하면 성공할 수 있을까? 오옥스 아이디 영패만 건네주면 되기 때문이다gcloud auth print-identity-token. 사용하기가 간단하다.이것은 Authorization 그것을 눈썹에 넣는 흔한 방법이다.
    curl -v -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://target-xxx.a.run.app/healthcheck
    - 中略
    * Using HTTP2, server supports multi-use
    * Connection state changed (HTTP/2 confirmed)
    * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
    * Using Stream ID: 1 (easy handle 0x558429803820)
    > GET /healthcheck HTTP/2
    > Host: target-xxx.a.run.app
    > user-agent: curl/7.68.0
    > accept: */*
    > authorization: Bearer xxxxx
    - 中略
    {"message":"OK","date":"2021-06-17T00:00:00"}
    
    요청이 성공했습니다.간단하죠?
    그럼 고로 이거 한번 해보자...그나저나 Go의 프로젝트에서 얻는 방법identity-token을 몰라서 길을 좀 잃었어요.
    아까 공식 매뉴얼을 봐도 메타데이터에서 얻은 거죠?
    tokenURL := fmt.Sprintf("/instance/service-accounts/default/identity?audience=%s", serviceURL)
    idToken, err := metadata.Get(tokenURL)
    
    이건 이렇게 하면 되지만 현지에서 이동이 안 돼서 힘들어요.나는 ADC를 활용하고 싶어서 스스로 조립하는 방향을 고려했다.
    여기는'어?'그렇게 생각하는 사람.아마 너는 옳다.그래서 너는 세 가지 선택이 있어.
    a) 계속 읽기: 다음
    b) 결론만 바라기: 전진idtoken
    c) 됐습니다: 브라우저를 가볍게 닫습니다.

    ADC


    GCP의 많은 프로그램 라이브러리는 응용 프로그램에 대응한다기본 인증 정보(ADC/Application Default Credentials).다음 순서에 따라 인증 정보를 확인하고 기본 인증을 합니다.
  • GOOGLE_APPLICATION_CREDENTIALS의 키 파일로 지정된 계정
  • 기본 경로에 있는 키 파일의 계정
  • GCP에서 실행되는 자원과 관련된 서비스 계정
  • 1, 2는 주로 로컬 개발 및 GCP 이외의 환경에서 사용됩니다.서비스 계정의 키 파일 GOOGLE_APPLICATION_CREDENTIALS 을 미리 가져와 경로를 넣는 흔한 방법입니다.
    3GCP 내 운영 환경에서 사용할 수 있습니다.Compute Engine, Google Kubernetes Engine, App Engine, Cloud Run, Cloud Function 등에 해당하는기본 계정 또는 사용자가 지정한 서비스 계정을 사용할 수 있습니다.이것은 서버의 로컬과 이미지 내부에 계정 인증 정보를 가지고 있을 필요가 없기 때문에 매우 편리하다.
    다만, 뒷면에서 ADC를 사용하는 프로그램 라이브러리는 있지만, 매뉴얼에서 ADC를 직접 사용하는 방법을 찾지 못했다.어쩔 수 없었기 때문에 Google의 라이브러리 GOOGLE_APPLICATION_CREDENTIALS 에서 사용한 곳을 찾아보니 다음과 같은 코드가 발견되었습니다.
    https://github.com/golang/oauth2/blob/master/google/default.go
    // FindDefaultCredentialsWithParams searches for "Application Default Credentials".
    //
    // It looks for credentials in the following places,
    // preferring the first location found:
    //
    //   1. A JSON file whose path is specified by the
    //      GOOGLE_APPLICATION_CREDENTIALS environment variable.
    //      For workload identity federation, refer to
    //      https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation on
    //      how to generate the JSON configuration file for on-prem/non-Google cloud
    //      platforms.
    //   2. A JSON file in a location known to the gcloud command-line tool.
    //      On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
    //      On other systems, $HOME/.config/gcloud/application_default_credentials.json.
    //   3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
    //      the appengine.AccessToken function.
    //   4. On Google Compute Engine, Google App Engine standard second generation runtimes
    //      (>= Go 1.11), and Google App Engine flexible environment, it fetches
    //      credentials from the metadata server.
    func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) {
    
    구체적인 사용 방법은 다음과 같다.간단하네.
    // ADC
    credentials, err := google.FindDefaultCredentials(oauth2.NoContext, AuthUrl)
    if err != nil {
    	fmt.Println(err)
    }
    config, err := google.JWTConfigFromJSON(credentials.JSON)
    if err != nil {
    	fmt.Println(err)
    }
    
    하지만 본 GCS의 라이브러리 등에 액세스 토큰이 생성돼 아이디 토큰을 직접 만들 수 없어 검색이 계속되고 있다.

    ID 토큰 생성


    우선 아래 두 가지를 참고하여 저는 ID 영패를 인증하기 위해 필요하지 않은 투척 코드를 썼습니다.특히 두 번째 링크는 바로 제가 하고 싶은 일이어서 참고 가치가 있습니다.
    https://cloud.google.com/endpoints/docs/openapi/service-account-authentication
    https://github.com/guillaumeblaquiere/token-generator
    상술한 내용을 참고하여 만든 코드는 다음과 같다.
    func main() {
    	url := "https://target-xxx.a.run.app/healthcheck"
    
    	token := google.IdToken(url)
    
    	client := &http.Client{}
    	req, _ := http.NewRequest("GET", url, nil)
    	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
    
    	res, _ := client.Do(req)
    	body, _ := io.ReadAll(res.Body)
    	defer res.Body.Close()
    	fmt.Println(string(body))
    }
    
    영패의 생성 자체는 이쪽 모듈과 분리된다.
    const AuthUrl = "https://www.googleapis.com/oauth2/v4/token"
    
    func IdToken(targetAudience string) string {
    
    	// ADC
    	credentials, err := google.FindDefaultCredentials(oauth2.NoContext, AuthUrl)
    	if err != nil {
    		fmt.Println(err)
    	}
    	config, err := google.JWTConfigFromJSON(credentials.JSON)
    	if err != nil {
    		fmt.Println(err)
    	}
    
    	// JWS
    	iat := time.Now()
    	exp := iat.Add(time.Hour)
    	cs := &jws.ClaimSet{
    		Iss: config.Email,
    		Sub: config.Email,
    		Aud: AuthUrl,
    		Iat: iat.Unix(),
    		Exp: exp.Unix(),
    	}
    	hdr := &jws.Header{
    		Algorithm: "RS256",
    		Typ:       "JWT",
    		KeyID:     config.PrivateKeyID,
    	}
    	privateKey := ParseKey(config.PrivateKey)
    
    	// Request Google OAuth server to get Token ID
    	cs.PrivateClaims = map[string]interface{}{"target_audience": targetAudience}
    	msg, err := jws.Encode(hdr, cs, privateKey)
    	if err != nil {
    		fmt.Println(fmt.Errorf("google: could not encode JWT: %v", err))
    	}
    
    	f := url.Values{
    		"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
    		"assertion":  {msg},
    	}
    	res, err := http.PostForm(AuthUrl, f)
    	if err != nil {
    		fmt.Println(err)
    	}
    
    	body, err := io.ReadAll(res.Body)
    	defer res.Body.Close()
    	if err != nil {
    		fmt.Println(err)
    	}
    
    	type resIdToken struct {
    		IdToken string `json:"id_token"`
    	}
    	id := &resIdToken{}
    	json.Unmarshal(body, id)
    
    	return id.IdToken
    }
    
    func ParseKey(key []byte) *rsa.PrivateKey {
    	block, _ := pem.Decode(key)
    	if block != nil {
    		key = block.Bytes
    	}
    	parsedKey, err := x509.ParsePKCS8PrivateKey(key)
    	if err != nil {
    		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
    		if err != nil {
    			return nil
    		}
    	}
    	parsed, ok := parsedKey.(*rsa.PrivateKey)
    	if !ok {
    		return nil
    	}
    	return parsed
    }
    
    포인트는 target_audiencePrivateClaims 같은 데 맡기는 거예요.나는 어떻게 너에게 줄지 몰라서 매우 괴롭다.마지막으로 GiitHub의 코드를 읽었는데 "쿠루토 카페"같은 느낌이 들었다.
    실행하면 서버의 반환 값을 아래와 같이 안전하게 표시할 수 있습니다.
    ❯ go run main.go
    {"message":"OK","date":"2021-06-18T00:00:00"}
    

    google.golang.org/api/idtoken


    그럼, "내가 하고 싶은 일을 했어!"이렇게 좋은 마음으로 다음 문서를 다시 봤다.
    https://cloud.google.com/run/docs/authenticating/service-to-service?hl=ja
    GCP 외부에서 호출된 엉망진창인 기록이 있어요!왜 처음부터 그걸 안 읽었을까...
    따라서 참고Identity-Aware Proxy 샘플 코드사용idtoken.NewClient의 작법은 다음과 같다.
    func main() {
    	url := "https://target-xxx.a.run.app/healthcheck"
    
    	ctx := context.Background()
    	client, _ := idtoken.NewClient(ctx, url)
    	req, _ := http.NewRequest("GET", url, nil)
    
    	res, _ := client.Do(req)
    	body, _ := io.ReadAll(res.Body)
    	defer res.Body.Close()
    	fmt.Println(string(body))
    }
    
    상당히 시원하네요!Authorization: Bearer 머리글이 작성되지 않았지만 자동으로 추가되었습니다.실행 결과는 다음과 같다.
    ❯ go run main.go
    {"message":"OK","date":"2021-06-18T00:00:00"}
    
    결과는 동일합니다!

    총결산


    Go에서 GCP 클라우드 런에서 서비스 간 인증을 받은 Identity Token은 어떻게 취득합니까?를 입력합니다.언어가 바뀌어도 기본적으로 같은 생각으로 사용해야 한다.
    하지만 부인할 수 없는 것은 처음 문서를 볼 때 잠이 혼미해져서 중요한 곳을 건너뛰고 시행착오를 많이 겪었기 때문이다...단순히 아이덴티티톡의 API를 취할 준비가 되지 않아 여러 바퀴를 돌았다.적어도 중간에 다시 읽으면 orz도 괜찮아요. 결과적으로 내용은 이미 알고 있으니까 괜찮은 것 같아요.
    그럼 해피 해킹!

    좋은 웹페이지 즐겨찾기