Using Golang Client to Operate Vault
Hello everyone, currently we’re going to try using golang vault client to create Write and Read secrets from vault.
For a brief introduction in the recent software engineering development, Vault is widely used for managing and storing sensitive data (ie: secret key, db password, etc). Most application has its configuration in order to define things how the app will run, for example let say we have simple application and it depends on this config.json
{
"app_name": "myApp",
"port": 8000,
"debug": false,
"db_username": "app_db_user",
"db_password": "this_is!@MyP@@sw0R00"
}
instead of saving the file to your repository in which making the db credentials “public-ly” available to everyone / people in your organization (as per your repository only available to your organization). We can put it into Vault, in which has a very restriction of who can access the vault.
The idea as an example of this writing is to change it into something like this:
- Removing the sensitive configuration from your config.json
- Have your Vault server ready
- Store the “secrets” using app / manually (only who authorized to use / access to Vault)
- The app can access / read the secrets
Let’s run the vault server locally using Docker image (https://hub.docker.com/_/vault/), we won’t be discussing a lot about how to host the vault server here.
docker run --cap-add=IPC_LOCK -e 'VAULT_LOCAL_CONFIG={"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:8200", "tls_disable": true}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true}' -p 8200:8200 hashicorp/vault server
After running, it will show us some logs like this
Let’s save the root token for now and testing later. Then, you can access your localhost:8200, it will need Token to “login” into the vault, use the Root Token, then you’re in
You can see cubbyhole and secrets. For simplicity let’s just take it for granted now that commonly your “sensitive data” will be put in the “secret” folder above.
https://developer.hashicorp.com/vault/docs/secrets/cubbyhole if you want to read more about cubbyhole.
Then, let’s create a simple Go application that will do these things:
- It has a usingToken function that will perform Write, and Read into the vault using Root Token mechanism
- It has usingAppRole function that will perform Read operation into the vault using AppRole mechanism
In order for us to do something within the vault, we need to be authorized. Using Root Token will give us like a “sudo” privilege and we can do anything.
While on app level especially production, it’s more recommended to use AppRole mechanism instead of using root token / sudo like. In short, the AppRole is a role that we set to be allowed doing spesific operation, in the spesific path. In this golang example, we’ll mostly use code based on the github readme (https://github.com/hashicorp/vault-client-go) with a slight modification for simplicity.
package main
import (
"context"
"log"
"time"
"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
)
func initVaultClient() *vault.Client {
// prepare a client with the given base address
client, err := vault.New(
vault.WithAddress("http://0.0.0.0:8200"),
vault.WithRequestTimeout(10*time.Second),
)
if err != nil {
log.Fatal(err)
}
return client
}
func main() {
usingToken()
}
func usingToken() {
cl := initVaultClient()
// example using Root Token
err := cl.SetToken("hvs.K5aUowkGeDwAyAgbo1mmcgGS") // THIS SHOULD NOT BE HARDCODED IN REAL APP, use os.Getenv() as example of more secured way of storing the token
if err != nil {
log.Fatal(err)
}
_, err = cl.Write(context.Background(), "/secret/data/myapp-secret", map[string]interface{}{
"data": map[string]interface{}{
"username": "username456",
"my_password": "wow this is amazing",
},
})
if err != nil {
log.Fatal(err)
}
log.Println("secret written successfully")
resp, err := cl.Read(context.Background(), "/secret/data/myapp-secret")
if err != nil {
log.Fatal(err)
}
data, ok := resp.Data["data"].(map[string]interface{})
if !ok {
log.Fatal("not map interface")
}
if data["username"] != nil {
log.Println("my username is", data["username"].(string))
}
if data["my_password"] != nil {
log.Println("my_password is", data["my_password"].(string))
}
}
These app does:
- initiate vault client
- set the token with root token
- Write key value secrets to the vault with path of “/secret/data/myapp-secret” that will exist in the vault as “/secret/myapp-secret”
- Read the value from the path (making sure it’s correct)
Vault SDK Read function will return response that is mostly map[string]interface data type, therefore we need to manually parse each of its data.
Run the application and we’ll see
PS C:\Users\Adit\Documents\Code\go\src\basic-vault> go run .\main.go
2023/06/09 17:50:08 secret written successfully
2023/06/09 17:50:08 my username is username456
2023/06/09 17:50:08 my_password is wow this is amazing
Checking in into the vault, and we’re seeing
Ok, using root token everything is working. But now let’s do more proper way of authenticating our application using App Role mechanism.
In order for us to use AppRole mechanism, we’ll need to conduct several steps:
- Enable the AppRole authentication method (in the vault)
- Create ACL Policy, this is to set what are allowed for the role we want to create
- Create a role that is bounded to the ACL policy below with some ttl configurations
- Generate a secret_id (like a password) for that role
- You’ll get a role_id, and secret_id that the app can use to authenticate
Since we’re not defining the parameter of secret_id_ttl
, then our secret_id (password like for a role) should be never expires.
Get the role_id
Generate (get) the secret_id
Then let’s write the function of using AppRole to test it out.
// the rest of main function same as before
func usingAppRole() {
cl := initVaultClient()
resp, err := cl.Auth.AppRoleLogin(
context.Background(),
schema.AppRoleLoginRequest{
RoleId: "60b94e80-0089-83c2-9e8f-6ec274526b47", // DONT Hardcode for real app, use Environment Var
SecretId: "eb3053ea-42d3-dc6c-3b22-9682ce20700e", // DONT Hardcode for real app, use Environment Var
},
)
if err != nil {
log.Fatal(err)
}
if err := cl.SetToken(resp.Auth.ClientToken); err != nil {
log.Fatal(err)
}
secretResp, err := cl.Read(context.Background(), "/secret/data/myapp-secret")
if err != nil {
log.Fatal(err)
}
data, ok := secretResp.Data["data"].(map[string]interface{})
if !ok {
log.Fatal("not map interface")
}
if data["username"] != nil {
log.Println("my username is", data["username"].(string))
}
if data["my_password"] != nil {
log.Println("my_password is", data["my_password"].(string))
}
}
Run the app and we’ll see
PS C:\Users\Adit\Documents\Code\go\src\basic-vault> go run .\main.go
2023/06/09 18:11:06 my username is username456
2023/06/09 18:11:06 my_password is wow this is amazing
There you go, now you can Store your secrets more safely in the Vault and fetch it rather than storing it “unsecurely” in the configuration file on your repository. Happy trying :)