Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added Certificate Authentication for Vault backend #659

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ AVP_USERNAME: Your Username
AVP_PASSWORD: Your Password
```

##### Certificate Authentication
For Certificate Authentication, these are the required parameters:
```
VAULT_ADDR: Your HashiCorp Vault Address
AVP_TYPE: vault
AVP_AUTH_TYPE: certificate
AVP_CERT: Your client certificate
AVP_KEY: Your client key
```

##### Examples

###### Path Annotation
Expand Down
7 changes: 4 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ We support all the backend specific environment variables each backend's SDK wil
We also support these AVP specific variables:

| Name | Description | Notes |
| -------------------------- |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|----------------------------|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AVP_TYPE | The type of Vault backend | Supported values: `vault`, `ibmsecretsmanager`, `awssecretsmanager`, `gcpsecretmanager`, `yandexcloudlockbox` and `1passwordconnect` |
| AVP_KV_VERSION | The vault secret engine | Supported values: `1` and `2` (defaults to 2). KV_VERSION will be ignored if the `avp.kubernetes.io/kv-version` annotation is present in a YAML resource. |
| AVP_AUTH_TYPE | The type of authentication | Supported values: vault: `approle, github, k8s, token`. Only honored for `AVP_TYPE` of `vault` |
| AVP_AUTH_TYPE | The type of authentication | Supported values: vault: `approle, github, k8s, token, certificate`. Only honored for `AVP_TYPE` of `vault` |
| AVP_GITHUB_TOKEN | Github token | Required with `AUTH_TYPE` of `github` |
| AVP_ROLE_ID | Vault AppRole Role_ID | Required with `AUTH_TYPE` of `approle` |
| AVP_SECRET_ID | Vault AppRole Secret_ID | Required with `AUTH_TYPE` of `approle` |
Expand All @@ -90,7 +90,8 @@ We also support these AVP specific variables:
| AVP_YCL_KEY_ID | Yandex Cloud Lockbox service account Key ID | Required with `TYPE` of `yandexcloudlockbox` |
| AVP_YCL_PRIVATE_KEY | Yandex Cloud Lockbox service account private key | Required with `TYPE` of `yandexcloudlockbox` |
| AVP_PATH_VALIDATION | Regular Expression to validate the Vault path | Optional. Can be used for e.g. to prevent path traversals. |

| AVP_CERT | Your Vault client certificate | Required with `AUTH_TYPE`of `certificate` |
| AVP_KEY | Your Vault client key | Required with `AUTH_TYPE`of `certificate` |
### Full List of Supported Annotation

We support several different annotations that can be used inside a kubernetes resource. These annotations will override any corresponding configuration set via Environment Variable or Configuration File.
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ require (
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gammazero/deque v0.2.1 // indirect
Expand Down Expand Up @@ -183,6 +184,7 @@ require (
github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect
github.com/hashicorp/go-secure-stdlib/nonceutil v0.1.0 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
github.com/hashicorp/go-secure-stdlib/password v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.3.0 // indirect
Expand All @@ -194,6 +196,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hashicorp/hcp-sdk-go v0.75.0 // indirect
github.com/hashicorp/mdns v1.0.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@ github.com/hashicorp/vault-testing-stepwise v0.1.4 h1:Lsv1KdpQyjhvmLgKeH65FG5MmY
github.com/hashicorp/vault-testing-stepwise v0.1.4/go.mod h1:Ym1T/kMM2sT6qgCIIJ3an7uaSWCJ8O7ohsWB9UiB5tI=
github.com/hashicorp/vault/api v1.12.0 h1:meCpJSesvzQyao8FCOgk2fGdoADAnbDu2WPJN1lDLJ4=
github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck=
github.com/hashicorp/vault/api/auth/userpass v0.1.0 h1:C6OdAYczMbzd1Pe1LLf2SHDulxOq/iybWV3kbgV/PS4=
github.com/hashicorp/vault/api/auth/userpass v0.1.0/go.mod h1:0orUbtkEwbEPmaQ+wvfrOddGBimLJnuN8A/J0PNfBks=
github.com/hashicorp/vault/sdk v0.12.0 h1:c2WeMWtF08zKQmrJya7paM4IVnsXIXF5UlhQTBdwZwQ=
github.com/hashicorp/vault/sdk v0.12.0/go.mod h1:2kN1F5owc/Yh1OwL32GGnYrX9E3vFOIKA/cGJxCNQ30=
github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw=
Expand Down
100 changes: 100 additions & 0 deletions pkg/auth/vault/certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package vault

import (
"fmt"
"github.com/argoproj-labs/argocd-vault-plugin/pkg/utils"
"github.com/hashicorp/vault/api"
"os"
)

const (
certificateMountPath = "auth/cert"
)

// CertificateAuth is a struct for working with Vault that uses certificate authentication
type CertificateAuth struct {
Certificate string
Key string
MountPath string
}

// NewCertificateAuth initalizes a new CertificateAuth with cert & key
func NewCertificateAuth(cert, key, mountPath string) *CertificateAuth {
certificateAuth := &CertificateAuth{
Certificate: cert,
Key: key,
MountPath: certificateMountPath,
}
if mountPath != "" {
certificateAuth.MountPath = mountPath
}

return certificateAuth
}

// Authenticate authenticates with Vault using userpass and returns a token
func (a *CertificateAuth) Authenticate(vaultClient *api.Client) error {
err := utils.LoginWithCachedToken(vaultClient)
if err != nil {
utils.VerboseToStdErr("Hashicorp Vault cannot retrieve cached token: %v. Generating a new one", err)
} else {
return nil
}

payload := map[string]interface{}{}

tempCrt, err := os.CreateTemp("", "vault_cert")
if err != nil {
return err
}
if _, err := tempCrt.WriteString(a.Certificate); err != nil {
return err
}
defer os.Remove(tempCrt.Name())

tempKey, err := os.CreateTemp("", "vault_key")
if err != nil {
return err
}

if _, err := tempKey.WriteString(a.Key); err != nil {
return err
}
defer os.Remove(tempKey.Name())

// Clone Client with new TLS Settings
apiClientConfig := vaultClient.CloneConfig()

tlsConfig := &api.TLSConfig{
ClientKey: tempKey.Name(),
ClientCert: tempCrt.Name(),
}

err = apiClientConfig.ConfigureTLS(tlsConfig)
if err != nil {
return err
}

certVaultClient, err := api.NewClient(apiClientConfig)

if err != nil {
return err
}

utils.VerboseToStdErr("Hashicorp Vault authenticating with certificate")

certVaultClient.ClearToken()
data, err := certVaultClient.Logical().Write(fmt.Sprintf("%s/login", a.MountPath), payload)
if err != nil {
return err
}

utils.VerboseToStdErr("Hashicorp Vault authentication response: %v", data)

// If we cannot write the Vault token, we'll just have to login next time. Nothing showstopping.
if err = utils.SetToken(vaultClient, data.Auth.ClientToken); err != nil {
utils.VerboseToStdErr("Hashicorp Vault cannot cache token for future runs: %v", err)
}

return nil
}
41 changes: 41 additions & 0 deletions pkg/auth/vault/certificate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package vault_test

import (
"bytes"
"testing"

"github.com/argoproj-labs/argocd-vault-plugin/pkg/auth/vault"
"github.com/argoproj-labs/argocd-vault-plugin/pkg/helpers"
"github.com/argoproj-labs/argocd-vault-plugin/pkg/utils"
)

func TestCertificateLogin(t *testing.T) {
cluster, _, _ := helpers.CreateTestCertificateVault(t)
defer cluster.Cleanup()

certificateAuth := vault.NewCertificateAuth("", "", "")

err := certificateAuth.Authenticate(cluster.Cores[0].Client)
if err != nil {
t.Fatalf("expected no errors but got: %s", err)
}

cachedToken, err := utils.ReadExistingToken()
if err != nil {
t.Fatalf("expected cached vault token but got: %s", err)
}

err = certificateAuth.Authenticate(cluster.Cores[0].Client)
if err != nil {
t.Fatalf("expected no errors but got: %s", err)
}

newCachedToken, err := utils.ReadExistingToken()
if err != nil {
t.Fatalf("expected cached vault token but got: %s", err)
}

if bytes.Compare(cachedToken, newCachedToken) != 0 {
t.Fatalf("expected same token %s but got %s", cachedToken, newCachedToken)
}
}
6 changes: 6 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ func New(v *viper.Viper, co *Options) (*Config, error) {
} else {
return nil, fmt.Errorf("%s and %s for userpass authentication cannot be empty", types.EnvAvpUsername, types.EnvAvpPassword)
}
case types.CertificateAuth:
if v.IsSet(types.EnvAvpCert) && v.IsSet(types.EnvAvpKey) {
auth = vault.NewCertificateAuth(v.GetString(types.EnvAvpCert), v.GetString(types.EnvAvpKey), v.GetString(types.EnvAvpMountPath))
} else {
return nil, fmt.Errorf("%s and %s for certificate authentication cannot be empty", types.EnvAvpCert, types.EnvAvpKey)
}
default:
return nil, fmt.Errorf("Must provide a supported Authentication Type, received %s", authType)
}
Expand Down
Loading