Skip to content

Commit

Permalink
Kubernetes auth example (Go) (#6)
Browse files Browse the repository at this point in the history
* Add Kubernetes auth example in Go

* Show issuer as part of config

* Make explanatory comment more concise

* Clarify issuer comment
  • Loading branch information
digivava authored Oct 1, 2021
1 parent 663b9b2 commit 32ac72b
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 5 deletions.
1 change: 0 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ jobs:
env:
VAULT_ADDR: http://0.0.0.0:8200
VAULT_DEV_ROOT_TOKEN_ID: testtoken
VAULT_TOKEN: testtoken
EXPECTED_SECRET_VALUE: Hashi123
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
vault/
.DS_Store
go/path/*
local-testing.sh
local-testing*
dotnet/ExampleTests/path/*

[Bb]in/
Expand Down
5 changes: 5 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Each line is a file pattern followed by one or more owners.
# More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners


* @hashicorp/vault-devex
2 changes: 1 addition & 1 deletion go/3_auth-approle-with-response-wrapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func getSecretWithAppRole() (string, error) {
"role_id": roleID,
"secret_id": secretID,
}
resp, err := client.Logical().Write("auth/approle/login", params)
resp, err := client.Logical().Write("auth/approle/login", params) // confirm with your Vault administrator that "approle" is the correct mount name
if err != nil {
return "", fmt.Errorf("unable to log in with approle: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/4_auth-aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func getSecretWithAWSAuthIAM() (string, error) {
params["role"] = "dev-role-iam" // the name of the role in Vault that was created with this IAM principal ARN bound to it

// log in to Vault's AWS auth method
resp, err := client.Logical().Write("auth/aws/login", params)
resp, err := client.Logical().Write("auth/aws/login", params) // confirm with your Vault administrator that "aws" is the correct mount name
if err != nil {
return "", fmt.Errorf("unable to log in with AWS IAM auth: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion go/5_auth-gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func getSecretWithGCPAuthIAM() (string, error) {

// Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to the path to a valid credentials JSON must be set,
// or Vault will fall back to Google's default instance credentials
resp, err := client.Logical().Write("auth/gcp/login", params)
resp, err := client.Logical().Write("auth/gcp/login", params) // confirm with your Vault administrator that "gcp" is the correct mount name
if err != nil {
return "", fmt.Errorf("unable to log in with GCP IAM auth: %w", err)
}
Expand Down
90 changes: 90 additions & 0 deletions go/6_auth-kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"fmt"
"os"

vault "github.com/hashicorp/vault/api"
)

// Fetches a key-value secret (kv-v2) after authenticating to Vault with a Kubernetes service account.
//
// As the client, all we need to do is pass along the JWT token representing our application's Kubernetes Service Account in our login request to Vault.
// This token is automatically mounted to your application's container by Kubernetes. Read more at https://www.vaultproject.io/docs/auth/kubernetes
//
// SETUP NOTES: If an operator has not already set up Kubernetes auth in Vault for you, then you must also first configure the Vault server with its own Service Account token to be able to communicate with the Kubernetes API
// so it can verify that the client's service-account token is valid. The service account that will be performing that verification needs the ClusterRole system:auth-delegator.
//
// export TOKEN_REVIEW_JWT=$(kubectl get secret $TOKEN_REVIEWER_SECRET --output='go-template={{ .data.token }}' | base64 --decode)
// export KUBE_HOST=$(kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.server}')
// kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.certificate-authority-data}' | base64 --decode > path/to/kube_ca_cert
//
// vault write auth/kubernetes/config \
// token_reviewer_jwt=${TOKEN_REVIEW_JWT} \
// kubernetes_host=${KUBE_HOST} \
// kubernetes_ca_cert=@path/to/kube_ca_cert \
// issuer="kubernetes/serviceaccount"
//
// The "issuer" field is normally only required when running Kubernetes 1.21 or above, and may differ from the default value above:
// https://www.vaultproject.io/docs/auth/kubernetes#discovering-the-service-account-issuer.
//
// Finally, make sure to create a role in Vault bound to your pod's service account:
//
// vault write auth/kubernetes/role/dev-role-k8s \
// policies="dev-policy" \
// bound_service_account_names="my-app" \
// bound_service_account_namespaces="default"
func getSecretWithKubernetesAuth() (string, error) {
// If set, the VAULT_ADDR environment variable will be the address that your pod uses to communicate with Vault.
config := vault.DefaultConfig() // modify for more granular configuration

client, err := vault.NewClient(config)
if err != nil {
return "", fmt.Errorf("unable to initialize Vault client: %w", err)
}

// Read the service-account token from the path where the token's Kubernetes Secret is mounted.
// By default, Kubernetes will mount this to /var/run/secrets/kubernetes.io/serviceaccount/token
// but an administrator may have configured it to be mounted elsewhere.
jwt, err := os.ReadFile("path/to/service-account-token")
if err != nil {
return "", fmt.Errorf("unable to read file containing service account token: %w", err)
}

params := map[string]interface{}{
"jwt": string(jwt),
"role": "dev-role-k8s", // the name of the role in Vault that was created with this app's Kubernetes service account bound to it
}

// log in to Vault's Kubernetes auth method
resp, err := client.Logical().Write("auth/kubernetes/login", params)
if err != nil {
return "", fmt.Errorf("unable to log in with Kubernetes auth: %w", err)
}
if resp == nil || resp.Auth == nil || resp.Auth.ClientToken == "" {
return "", fmt.Errorf("login response did not return client token")
}

// now you will use the resulting Vault token for making all future calls to Vault
client.SetToken(resp.Auth.ClientToken)

// get secret from Vault
secret, err := client.Logical().Read("kv-v2/data/creds")
if err != nil {
return "", fmt.Errorf("unable to read secret: %w", err)
}

data, ok := secret.Data["data"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("data type assertion failed: %T %#v", secret.Data["data"], secret.Data["data"])
}

// data map can contain more than one key-value pair, in this case we're just grabbing one of them
key := "password"
value, ok := data[key].(string)
if !ok {
return "", fmt.Errorf("value type assertion failed: %T %#v", data[key], data[key])
}

return value, nil
}
13 changes: 13 additions & 0 deletions go/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,16 @@ func TestGetSecretWithGCPAuthIAM(t *testing.T) {
t.Fatalf("Expected %s, but got %s", expected, value)
}
}

func TestGetSecretWithKubernetesAuth(t *testing.T) {
if os.Getenv("LOCAL_TESTING") == "" {
t.Skip("skipping test in CI for now")
}
value, err := getSecretWithKubernetesAuth()
if err != nil {
t.Fatalf("Failed to get secret using Kubernetes service account: %v", err)
}
if value != expected {
t.Fatalf("Expected %s, but got %s", expected, value)
}
}

0 comments on commit 32ac72b

Please sign in to comment.