Carpe Diem

備忘録

GoでVaultを操作

概要

これまで紹介したHashiCorp Vaultの使い方はCLIを使うのがメインでしたが、実際はアプリケーション内で秘密情報を扱うケースが多々あります。
VaultはGoのライブラリを提供しているので、様々なログイン方法を紹介しつつ秘密情報にアクセスしてみます。

環境

CLIから秘密情報を登録する

あらかじめKey/Valueに秘密情報を登録しておきます。

$ vault kv put secret/my-secret my-value=s3cr3t
Key              Value
---              -----
created_time     2018-07-13T15:01:30.440745605Z
deletion_time    n/a
destroyed        false
version          2

登録されているか確認します。

$ vault kv get secret/my-secret
====== Metadata ======
Key              Value
---              -----
created_time     2018-07-13T15:01:30.440745605Z
deletion_time    n/a
destroyed        false
version          2

====== Data ======
Key         Value
---         -----
my-value    s3cr3t

登録されてますね。

Goでアクセス

通常の使い方(固定トークン)

github.com/hashicorp/vault/apiというライブラリを提供しているのでそれを使います。
クライアントを用意し、それにトークンを渡せばCLIのようなコマンドが各種使えます。
まずは基本的な固定トークンでの使用方法です。

const (
    vaultAddr = "http://YOUR_VAULT_ADDR:8200"

    staticToken = "YOUR_STATIC_TOKEN"
)

var httpClient = &http.Client{
    Timeout: 10 * time.Second,
}

func main() {
    token := staticToken

    client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
    if err != nil {
        panic(err)
    }

    client.SetToken(token)
    data, err := client.Logical().Read("secret/data/my-secret")
    if err != nil {
        panic(err)
    }

    b, _ := json.Marshal(data.Data)
    fmt.Println(string(b))
}

実行

$ go run main.go | jq .
{
  "data": {
    "my-value": "s3cr3t"
  },
  "metadata": {
    "created_time": "2018-07-13T15:01:30.440745605Z",
    "deletion_time": "",
    "destroyed": false,
    "version": 2
  }
}

ちゃんと秘密情報を取得できました。

User&Passwordでログイン

固定トークンでなく、userpass authで認証してからトークンを発行し、それを使うようにしてみます。

func main() {
    token, err := userpassLogin()
    if err != nil {
        panic(err)
    }

    client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
    if err != nil {
        panic(err)
    }

    client.SetToken(token)
    data, err := client.Logical().Read("secret/data/my-secret")
    if err != nil {
        panic(err)
    }

    b, _ := json.Marshal(data.Data)
    fmt.Println(string(b))
}

const (
    username = "jun06t"
    password = "foo"
)

func userpassLogin() (string, error) {
    // create a vault client
    client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
    if err != nil {
        return "", err
    }

    // to pass the password
    options := map[string]interface{}{
        "password": password,
    }
    path := fmt.Sprintf("auth/userpass/login/%s", username)

    // PUT call to get a token
    secret, err := client.Logical().Write(path, options)
    if err != nil {
        return "", err
    }

    token := secret.Auth.ClientToken
    return token, nil
}

実行

$ go run main.go | jq .
{
  "data": {
    "my-value": "s3cr3t"
  },
  "metadata": {
    "created_time": "2018-07-13T15:01:30.440745605Z",
    "deletion_time": "",
    "destroyed": false,
    "version": 2
  }
}

こちらも問題なく取得できました。

AWSのIAMでログイン

前回ローカルのCLIからIAMを使ってログインする方法を紹介しました。

christina04.hatenablog.com

今回はGoからログインします。

func main() {
    token, err := awsLogin()
    if err != nil {
        panic(err)
    }

    client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
    if err != nil {
        panic(err)
    }

    client.SetToken(token)
    data, err := client.Logical().Read("secret/data/my-secret")
    if err != nil {
        panic(err)
    }

    b, _ := json.Marshal(data.Data)
    fmt.Println(string(b))
}

const (
    accessKey    = ""
    secretKey    = ""
    sessionToken = ""
    headerValue  = ""
)

func awsLogin() (string, error) {
    // get aws credential
    data, err := awsauth.GenerateLoginData(accessKey, secretKey, sessionToken, headerValue)
    if err != nil {
        return "", err
    }

    // create a vault client
    client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
    if err != nil {
        return "", err
    }

    // PUT call to get a token
    secret, err := client.Logical().Write("auth/aws/login", data)
    if err != nil {
        return "", err
    }

    token := secret.Auth.ClientToken
    return token, nil
}

実行

こちらも問題なく取得できました。

$ go run main.go | jq .
{
  "data": {
    "my-value": "s3cr3t"
  },
  "metadata": {
    "created_time": "2018-07-13T15:01:30.440745605Z",
    "deletion_time": "",
    "destroyed": false,
    "version": 2
  }
}

クレデンシャル情報は予め取得して置く必要がある

上のコードから分かるように、github.com/hashicorp/vault/builtin/credential/awsGenerateLoginDataというメソッドを使っています。
CLIのコマンドだと

$ vault login -method=aws

で済んだので、パス(auth/aws/login)さえ指定すれば良いのかなと思いましたがダメでした。

auditログを見てみると

CLIのログ

{
  "time": "2018-07-13T01:37:57.465134866Z",
  "type": "request",
  "auth": {
    "client_token": "",
    "accessor": "",
    "display_name": "",
    "policies": null,
    "metadata": null,
    "entity_id": ""
  },
  "request": {
    "id": "b5a58f9a-8dc8-90bd-34e7-5f55b22518ab",
    "operation": "update",
    "client_token": "",
    "client_token_accessor": "",
    "path": "auth/aws/login",
    "data": {
      "iam_http_request_method": "hmac-sha256:386d03a363ce39bc27a97ceb9f507ae5ad2251a2564d333ba2e13efcd8010384",
      "iam_request_body": "hmac-sha256:e53839a5a1f36971746b894c1b15d2f3f5dadbfa14f2967f0d5450c8bd27ead6",
      "iam_request_headers": "hmac-sha256:cea3204364b8295fb82512b7cc18065f004c18beaace6e11591d51d8da79125d",
      "iam_request_url": "hmac-sha256:8bfaab975daaf27f7e5cdfc10e51c50c50fe0f40243f821116d3e9c6a9657db7",
      "role": "hmac-sha256:6e72c3db169bfab3b7939815a865933969bfcef9925e07c6a8cb91a0cd4c735e"
    },
    "policy_override": false,
    "remote_address": "10.74.0.242",
    "wrap_ttl": 0,
    "headers": {}
  },
  "error": ""
}

GenerateLoginData()を使わなかったGoのログ

{
  "time": "2018-07-13T01:36:49.420007917Z",
  "type": "request",
  "auth": {
    "client_token": "",
    "accessor": "",
    "display_name": "",
    "policies": null,
    "metadata": null,
    "entity_id": ""
  },
  "request": {
    "id": "864c6c6e-f9ce-4097-174a-5261bda3c42b",
    "operation": "update",
    "client_token": "",
    "client_token_accessor": "",
    "path": "auth/aws/login",
    "data": null,
    "policy_override": false,
    "remote_address": "10.74.0.242",
    "wrap_ttl": 0,
    "headers": {}
  },
  "error": ""
}

このようにクレデンシャルが無いことが分かります。
なのでクレデンシャルを取得するメソッドがないかなぁと探したところ上記のメソッドを見つけました。

ソース上はどうなってる?

このメソッドのソースを見てみると

func GenerateLoginData(accessKey, secretKey, sessionToken, headerValue string) (map[string]interface{}, error) {
    loginData := make(map[string]interface{})

    credConfig := &awsutil.CredentialsConfig{
        AccessKey:    accessKey,
        SecretKey:    secretKey,
        SessionToken: sessionToken,
    }
    creds, err := credConfig.GenerateCredentialChain()

vault/cli.go at bd9ba940ef4180af5790325e0eaff7e7c3ce1cc2 · hashicorp/vault · GitHub

となっており、accessKey等が明示的にあればそれを使いますが、無ければGenerateCredentialChainではaws-sdk-goのデフォルトのクレデンシャルの扱いのように

  • EnvProvider:環境変数
  • SharedCredentialsProvider:~/.aws/credentials
  • RemoteCredProvider:IAM Role

といったProviderを使ってクレデンシャルを取得するようになっています。

vault/generate_credentials.go at ce1b43cd487a3fa47734af3a68e270d15fa02f77 · hashicorp/vault · GitHub

なのでローカル開発では~/.aws/credentialsを使い、実際のサーバ上ではEC2 Roleを使えるのでアプリケーションコードのどこにもトークン等をべた書きする必要がありません

まとめ

Goからvault serverにログインして秘密情報を取得する方法を紹介しました。
特に一番最後の方法では「結局vault用のトークンやuser&passがどこかにべた書きされてしまう」といった問題が無く、かつローカル開発でも支障なく使えるメリットがあるのでどんどん使っていきたいですね。

今回のコードは以下のgistにまとめてあります。
Golang Vault Login Sample · GitHub

ソース