Skip to content

Commit

Permalink
vault kubernetes auth support
Browse files Browse the repository at this point in the history
  • Loading branch information
fcgravalos authored and eduardogr committed Dec 14, 2020
1 parent 065580f commit 88a9e60
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 10 deletions.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ To deploy it just run `kubectl apply -f secretdefinition-sample.yaml`
| `vault.role-id` | `""` | Vault appRole `role_id`. `VAULT_ROLE_ID` environment would take precedence. |
| `vault.secret-id` | `""` | Vault appRole `secret_id`. `VAULT_SECRET_ID` environment would take precedence. |
| `vault.engine` | kv2 | Vault secrets engine to use. Only key/value engines supported. Default is kv version 2 |
| `vault.approle-path` | approle | Vault approle path |
| `vault.auth-method` | approle | Vault authentication method. Supported: approle, kubernetes. |
| `vault.approle-path` | approle | Vault approle login path |
| `vault.kubernetes-path` | kubernetes | Vault kubernetes login path |
| `vault.kubernetes-role` | `""` | Vault kubernetes role name |
| `vault.max-token-ttl` | 300 |Max seconds to consider a token expired. |
| `vault.token-polling-period` | 15s | Polling interval to check token expiration time. |
| `vault.renew-ttl-increment` | 600 | TTL time for renewed token. |
Expand Down Expand Up @@ -168,13 +171,32 @@ To get the `role_id`:

`$ vault read auth/approle/role/secrets-manager/role-id`

### Vault Kubernetes Authentication
In addition to `appRole`, `secrets-manager` can authenticate to Vault using its own Kubernetes `serviceAccount`. Follow the [Vault Kubernetes auth guide](https://www.vaultproject.io/docs/auth/kubernetes) to enable it and configure it.

Example:

```sh
$ cat > secrets-manager-role.json <<EOF
{
"bound_service_account_names": [ "secrets-manager" ],
"bound_service_account_namespaces": [ "my-namesapace" ],
"policies": [ "my-policy" ],
"max_ttl": 3600
}
EOF

$ vault write auth/kubernetes/role/secrets-manager @secrets-manager-role.json
```


## Versioning

Right now versioning it's a manually task.
Depending on the kind of the update we would apply a major, minor or patch update given that we follow [semantic versionin](https://semver.org/).

Before building release images, we should run one of the following commands:
- make update-major-version
- make update-major-version
- make update-minor-version
- make update-patch-version

Expand Down
3 changes: 3 additions & 0 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ func init() {
type Config struct {
BackendTimeout time.Duration
VaultURL string
VaultAuthMethod string
VaultRoleID string
VaultSecretID string
VaultKubernetesRole string
VaultMaxTokenTTL int64
VaultTokenPollingPeriod time.Duration
VaultRenewTTLIncrement int
VaultEngine string
VaultApprolePath string
VaultKubernetesPath string
}

// Client interface represent a backend client interface that should be implemented
Expand Down
49 changes: 48 additions & 1 deletion backend/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"time"

Expand All @@ -15,22 +18,46 @@ import (

var vMetrics *vaultMetrics

const defaultSecretKey = "data"
const (
defaultSecretKey = "data"
kubernetesJwtTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
kubernetesAuthMethod = "kubernetes"
appRoleAuthMethod = "approle"
)

type client struct {
vclient *api.Client
logical *api.Logical
roleID string
authMethod string
secretID string
kubernetesRole string
maxTokenTTL int64
tokenPollingPeriod time.Duration
renewTTLIncrement int
engine engine
approlePath string
kubernetesPath string
logger logr.Logger
}

func (c *client) vaultLogin() error {
switch c.authMethod {
case appRoleAuthMethod:
return c.vaultAppRoleLogin()
case kubernetesAuthMethod:
fd, err := os.Open(kubernetesJwtTokenPath)
defer fd.Close()
if err != nil {
return err
}
return c.vaultKubernetesLogin(fd)
default:
return c.vaultAppRoleLogin()
}
}

func (c *client) vaultAppRoleLogin() error {
appRole := map[string]interface{}{
"role_id": c.roleID,
"secret_id": c.secretID,
Expand All @@ -43,6 +70,23 @@ func (c *client) vaultLogin() error {
return nil
}

func (c *client) vaultKubernetesLogin(podSATokenReader io.Reader) error {
jwt, err := ioutil.ReadAll(podSATokenReader)
if err != nil {
return err
}
kubernetes := map[string]interface{}{
"jwt": string(jwt),
"role": c.kubernetesRole,
}
resp, err := c.logical.Write(fmt.Sprintf("auth/%s/login", c.kubernetesPath), kubernetes)
if err != nil {
return err
}
c.vclient.SetToken(resp.Auth.ClientToken)
return nil
}

func vaultClient(l logr.Logger, cfg Config) (*client, error) {
logger := l.WithName("vault").WithValues(
"vault_url", cfg.VaultURL,
Expand All @@ -69,13 +113,16 @@ func vaultClient(l logr.Logger, cfg Config) (*client, error) {
client := client{
vclient: vclient,
logical: logical,
authMethod: cfg.VaultAuthMethod,
roleID: cfg.VaultRoleID,
secretID: cfg.VaultSecretID,
kubernetesRole: cfg.VaultKubernetesRole,
maxTokenTTL: cfg.VaultMaxTokenTTL,
tokenPollingPeriod: cfg.VaultTokenPollingPeriod,
renewTTLIncrement: cfg.VaultRenewTTLIncrement,
engine: engine,
approlePath: cfg.VaultApprolePath,
kubernetesPath: cfg.VaultKubernetesPath,
}

err = client.vaultLogin()
Expand Down
67 changes: 62 additions & 5 deletions backend/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"

"github.com/go-logr/logr"
"github.com/gorilla/mux"
"github.com/hashicorp/vault/api"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/tuenti/secrets-manager/errors"
Expand All @@ -33,14 +35,17 @@ const (
defaultTokenRenewable = true
defaultRevokedToken = false
defaultInvalidAppRole = false
defaultKubernetesRole = false
fakeKubernetesSAToken = `eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh`
)

type testConfig struct {
tokenTTL int
tokenRenewable bool
tokenRevoked bool
invalidRoleID bool
invalidSecretID bool
tokenTTL int
tokenRenewable bool
tokenRevoked bool
invalidRoleID bool
invalidSecretID bool
invalidKubernetesRole bool
}

var (
Expand Down Expand Up @@ -162,6 +167,39 @@ func v1AuthTokenRenewSelf(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}

func v1AuthKubernetesLogin(w http.ResponseWriter, r *http.Request) {
var response interface{}
jsonData := ""
if !testCfg.invalidKubernetesRole {
jsonData = fmt.Sprintf(`
{
"auth": {
"client_token": "%s",
"accessor": "78e87a38-84ed-2692-538f-ca8b9f400ab3",
"policies": ["secrets-manager"],
"metadata": {
"role": "secrets-manager",
"service_account_name": "secrets-manager",
"service_account_namespace": "default",
"service_account_secret_name": "secrets-manager-token-pd21c",
"service_account_uid": "aa9aa8ff-98d0-11e7-9bb7-0800276d99bf"
},
"lease_duration": 2764800,
"renewable": true
}
}`, fakeToken)
} else {
jsonData = `{"errors":["forbidden"]}`
w.WriteHeader(http.StatusForbidden)
}
if err := json.Unmarshal([]byte(jsonData), &response); err != nil {
fmt.Printf("unable to unmarshal json %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

func v1AuthAppRoleLogin(w http.ResponseWriter, r *http.Request) {
var response interface{}
jsonData := ""
Expand Down Expand Up @@ -266,6 +304,24 @@ func v1SecretTestKv1(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
}

func TestVaultLoginKubernetes(t *testing.T) {
httpClient := new(http.Client)
vclient, _ := api.NewClient(&api.Config{Address: vaultCfg.VaultURL, HttpClient: httpClient})
c := &client{
vclient: vclient,
logical: vclient.Logical(),
authMethod: "kubernetes",
kubernetesRole: "secrets-manager",
kubernetesPath: "kubernetes",
}
err := c.vaultKubernetesLogin(strings.NewReader(fakeKubernetesSAToken))
assert.Nil(t, err)
mutex.Lock()
defer mutex.Unlock()
testCfg.invalidKubernetesRole = true
err2 := c.vaultKubernetesLogin(strings.NewReader(fakeKubernetesSAToken))
assert.NotNil(t, err2)
}
func TestVaultBackendInvalidCfg(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down Expand Up @@ -491,6 +547,7 @@ func TestMain(m *testing.M) {
v1AuthHandler.HandleFunc("/token/lookup-self", v1AuthTokenLookupSelf).Methods("GET")
v1AuthHandler.HandleFunc("/token/renew-self", v1AuthTokenRenewSelf).Methods("PUT")
v1AuthHandler.HandleFunc("/approle/login", v1AuthAppRoleLogin).Methods("PUT")
v1AuthHandler.HandleFunc("/kubernetes/login", v1AuthKubernetesLogin).Methods("PUT")
v1SecretHandler.HandleFunc("/data/test", v1SecretTestKv2).Methods("GET")
v1SecretHandler.HandleFunc("/test", v1SecretTestKv1).Methods("GET")

Expand Down
2 changes: 1 addition & 1 deletion deploy/version/version.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=v1.0.2
version=v1.0.3-kubernetes-role-3
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
Expand Down
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,16 @@ func main() {
flag.DurationVar(&reconcilePeriod, "reconcile-period", 5*time.Second, "How often the controller will re-queue secretdefinition events")
flag.DurationVar(&backendCfg.BackendTimeout, "config.backend-timeout", 5*time.Second, "Backend connection timeout")
flag.StringVar(&backendCfg.VaultURL, "vault.url", "https://127.0.0.1:8200", "Vault address. VAULT_ADDR environment would take precedence.")
flag.StringVar(&backendCfg.VaultAuthMethod, "vault.auth-method", "approle", "Vault authentication method. Supported: approle, kubernetes.")
flag.StringVar(&backendCfg.VaultRoleID, "vault.role-id", "", "Vault approle role id. VAULT_ROLE_ID environment would take precedence.")
flag.StringVar(&backendCfg.VaultSecretID, "vault.secret-id", "", "Vault approle secret id. VAULT_SECRET_ID environment would take precedence.")
flag.StringVar(&backendCfg.VaultKubernetesRole, "vault.kubernetes-role", "", "Vault kubernetes role name.")
flag.Int64Var(&backendCfg.VaultMaxTokenTTL, "vault.max-token-ttl", 300, "Max seconds to consider a token expired.")
flag.DurationVar(&backendCfg.VaultTokenPollingPeriod, "vault.token-polling-period", 15*time.Second, "Polling interval to check token expiration time.")
flag.IntVar(&backendCfg.VaultRenewTTLIncrement, "vault.renew-ttl-increment", 600, "TTL time for renewed token.")
flag.StringVar(&backendCfg.VaultEngine, "vault.engine", "kv2", "Vault secret engine. Only KV version 1 and 2 supported")
flag.StringVar(&backendCfg.VaultApprolePath, "vault.approle-path", "approle", "Vault approle path")
flag.StringVar(&backendCfg.VaultApprolePath, "vault.approle-path", "approle", "Vault approle login path")
flag.StringVar(&backendCfg.VaultKubernetesPath, "vault.kubernetes-path", "kubernetes", "Vault kubernetes login path")
flag.StringVar(&watchNamespaces, "watch-namespaces", "", "Comma separated list of namespaces that secrets-manager will watch for SecretDefinitions. By default all namespaces are watched.")
flag.StringVar(&excludeNamespaces, "exclude-namespaces", "", "Comma separated list of namespaces that secrets-manager will not watch for SecretDefinitions. By default all namespaces are watched.")
flag.Parse()
Expand Down

0 comments on commit 88a9e60

Please sign in to comment.