Skip to content

Commit

Permalink
NOISSUE - Vault operations with app role authentication (#2084)
Browse files Browse the repository at this point in the history
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: arvindh123 <arvindh91@gmail.com>
  • Loading branch information
arvindh123 authored Feb 20, 2024
1 parent 4c206ec commit ab4206c
Show file tree
Hide file tree
Showing 23 changed files with 764 additions and 268 deletions.
77 changes: 41 additions & 36 deletions certs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,38 +30,41 @@ curl -s -S -X DELETE http://localhost:9019/certs/revoke -H "Authorization: Beare

The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.

| Variable | Description | Default |
| ------------------------- | --------------------------------------------------------------------------- | ----------------------------------- |
| MG_CERTS_LOG_LEVEL | Log level for the Certs (debug, info, warn, error) | info |
| MG_CERTS_HTTP_HOST | Service Certs host | "" |
| MG_CERTS_HTTP_PORT | Service Certs port | 9019 |
| MG_CERTS_HTTP_SERVER_CERT | Path to the PEM encoded server certificate file | "" |
| MG_CERTS_HTTP_SERVER_KEY | Path to the PEM encoded server key file | "" |
| MG_AUTH_GRPC_URL | Auth service gRPC URL | <localhost:8181> |
| MG_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service gRPC client certificate file | "" |
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service gRPC client key file | "" |
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server gRPC server trusted CA certificate file | "" |
| MG_CERTS_SIGN_CA_PATH | Path to the PEM encoded CA certificate file | ca.crt |
| MG_CERTS_SIGN_CA_KEY_PATH | Path to the PEM encoded CA key file | ca.key |
| MG_CERTS_VAULT_HOST | Vault host | "" |
| MG_VAULT_PKI_INT_PATH | Vault PKI intermediate path | pki_int |
| MG_VAULT_CA_ROLE_NAME | Vault PKI role name | magistrala |
| MG_VAULT_TOKEN | Vault token | "" |
| MG_CERTS_DB_HOST | Database host | localhost |
| MG_CERTS_DB_PORT | Database port | 5432 |
| MG_CERTS_DB_PASS | Database password | magistrala |
| MG_CERTS_DB_USER | Database user | magistrala |
| MG_CERTS_DB_NAME | Database name | certs |
| MG_CERTS_DB_SSL_MODE | Database SSL mode | disable |
| MG_CERTS_DB_SSL_CERT | Database SSL certificate | "" |
| MG_CERTS_DB_SSL_KEY | Database SSL key | "" |
| MG_CERTS_DB_SSL_ROOT_CERT | Database SSL root certificate | "" |
| MG_THINGS_URL | Things service URL | <localhost:9000> |
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:14268/api/traces> |
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
| MG_CERTS_INSTANCE_ID | Service instance ID | "" |

| Variable | Description | Default |
| :---------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| MG_CERTS_LOG_LEVEL | Log level for the Certs (debug, info, warn, error) | info |
| MG_CERTS_HTTP_HOST | Service Certs host | "" |
| MG_CERTS_HTTP_PORT | Service Certs port | 9019 |
| MG_CERTS_HTTP_SERVER_CERT | Path to the PEM encoded server certificate file | "" |
| MG_CERTS_HTTP_SERVER_KEY | Path to the PEM encoded server key file | "" |
| MG_AUTH_GRPC_URL | Auth service gRPC URL | [localhost:8181](localhost:8181) |
| MG_AUTH_GRPC_TIMEOUT | Auth service gRPC request timeout in seconds | 1s |
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service gRPC client certificate file | "" |
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service gRPC client key file | "" |
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server gRPC server trusted CA certificate file | "" |
| MG_CERTS_SIGN_CA_PATH | Path to the PEM encoded CA certificate file | ca.crt |
| MG_CERTS_SIGN_CA_KEY_PATH | Path to the PEM encoded CA key file | ca.key |
| MG_CERTS_VAULT_HOST | Vault host | http://vault:8200 |
| MG_CERTS_VAULT_NAMESPACE | Vault namespace in which pki is present | magistrala |
| MG_CERTS_VAULT_APPROLE_ROLEID | Vault AppRole auth RoleID | magistrala |
| MG_CERTS_VAULT_APPROLE_SECRET | Vault AppRole auth Secret | magistrala |
| MG_CERTS_VAULT_THINGS_CERTS_PKI_PATH | Vault PKI path for issuing Things Certificates | pki_int |
| MG_CERTS_VAULT_THINGS_CERTS_PKI_ROLE_NAME | Vault PKI Role Name for issuing Things Certificates | magistrala_things_certs |
| MG_CERTS_DB_HOST | Database host | localhost |
| MG_CERTS_DB_PORT | Database port | 5432 |
| MG_CERTS_DB_PASS | Database password | magistrala |
| MG_CERTS_DB_USER | Database user | magistrala |
| MG_CERTS_DB_NAME | Database name | certs |
| MG_CERTS_DB_SSL_MODE | Database SSL mode | disable |
| MG_CERTS_DB_SSL_CERT | Database SSL certificate | "" |
| MG_CERTS_DB_SSL_KEY | Database SSL key | "" |
| MG_CERTS_DB_SSL_ROOT_CERT | Database SSL root certificate | "" |
| MG_THINGS_URL | Things service URL | [localhost:9000](localhost:9000) |
| MG_JAEGER_URL | Jaeger server URL | [http://localhost:14268/api/traces](http://localhost:14268/api/traces) |
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
| MG_CERTS_INSTANCE_ID | Service instance ID | "" |

## Deployment

Expand Down Expand Up @@ -95,10 +98,12 @@ MG_AUTH_GRPC_CLIENT_KEY="" \
MG_AUTH_GRPC_SERVER_CERTS="" \
MG_CERTS_SIGN_CA_PATH=ca.crt \
MG_CERTS_SIGN_CA_KEY_PATH=ca.key \
MG_CERTS_VAULT_HOST="" \
MG_VAULT_PKI_INT_PATH=pki_int \
MG_VAULT_CA_ROLE_NAME=magistrala \
MG_VAULT_TOKEN="" \
MG_CERTS_VAULT_HOST=http://vault:8200 \
MG_CERTS_VAULT_NAMESPACE=magistrala \
MG_CERTS_VAULT_APPROLE_ROLEID=magistrala \
MG_CERTS_VAULT_APPROLE_SECRET=magistrala \
MG_CERTS_VAULT_THINGS_CERTS_PKI_PATH=pki_int \
MG_CERTS_VAULT_THINGS_CERTS_PKI_ROLE_NAME=magistrala_things_certs \
MG_CERTS_DB_HOST=localhost \
MG_CERTS_DB_PORT=5432 \
MG_CERTS_DB_PASS=magistrala \
Expand Down
5 changes: 5 additions & 0 deletions certs/mocks/pki.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package mocks
import (
"bufio"
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
Expand Down Expand Up @@ -160,6 +161,10 @@ func (a *agent) Revoke(serial string) (time.Time, error) {
return time.Now(), nil
}

func (a *agent) LoginAndRenew(ctx context.Context) error {
return nil
}

func publicKey(priv interface{}) (interface{}, error) {
if priv == nil {
return nil, errPrivateKeyEmpty
Expand Down
109 changes: 105 additions & 4 deletions certs/pki/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
package pki

import (
"context"
"encoding/json"
"log/slog"
"time"

"github.com/absmach/magistrala/pkg/errors"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
"github.com/mitchellh/mapstructure"
)

Expand All @@ -30,6 +33,13 @@ var (
ErrFailedCertRevocation = errors.New("failed to revoke certificate")

errFailedCertDecoding = errors.New("failed to decode response from vault service")
errFailedToLogin = errors.New("failed to login to Vault")
errFailedAppRole = errors.New("failed to create vault new app role")
errNoAuthInfo = errors.New("no auth information from Vault")
errNonRenewal = errors.New("token is not configured to be renewable")
errRenewWatcher = errors.New("unable to initialize new lifetime watcher for renewing auth token")
errFailedRenew = errors.New("failed to renew token")
errCouldNotRenew = errors.New("token can no longer be renewed")
)

type Cert struct {
Expand All @@ -52,17 +62,24 @@ type Agent interface {

// Revoke revokes certificate from PKI
Revoke(serial string) (time.Time, error)

// Login to PKI and renews token
LoginAndRenew(ctx context.Context) error
}

type pkiAgent struct {
token string
appRole string
appSecret string
namespace string
path string
role string
host string
issueURL string
readURL string
revokeURL string
client *api.Client
secret *api.Secret
logger *slog.Logger
}

type certReq struct {
Expand All @@ -75,21 +92,27 @@ type certRevokeReq struct {
}

// NewVaultClient instantiates a Vault client.
func NewVaultClient(token, host, path, role string) (Agent, error) {
func NewVaultClient(appRole, appSecret, host, namespace, path, role string, logger *slog.Logger) (Agent, error) {
conf := api.DefaultConfig()
conf.Address = host

client, err := api.NewClient(conf)
if err != nil {
return nil, err
}
client.SetToken(token)
if namespace != "" {
client.SetNamespace(namespace)
}

p := pkiAgent{
token: token,
appRole: appRole,
appSecret: appSecret,
host: host,
namespace: namespace,
role: role,
path: path,
client: client,
logger: logger,
issueURL: "/" + path + "/" + issue + "/" + role,
readURL: "/" + path + "/" + cert + "/",
revokeURL: "/" + path + "/" + revoke,
Expand Down Expand Up @@ -162,3 +185,81 @@ func (p *pkiAgent) Revoke(serial string) (time.Time, error) {

return time.Unix(0, int64(rev)*int64(time.Second)), nil
}

func (p *pkiAgent) LoginAndRenew(ctx context.Context) error {
for {
select {
case <-ctx.Done():
p.logger.Info("pki login and renew function stopping")
return nil
default:
err := p.login(ctx)
if err != nil {
p.logger.Info("unable to authenticate to Vault", slog.Any("error", err))
time.Sleep(5 * time.Second)
break
}
tokenErr := p.manageTokenLifecycle()
if tokenErr != nil {
p.logger.Info("unable to start managing token lifecycle", slog.Any("error", tokenErr))
time.Sleep(5 * time.Second)
}
}
}
}

func (p *pkiAgent) login(ctx context.Context) error {
secretID := &approle.SecretID{FromString: p.appSecret}

authMethod, err := approle.NewAppRoleAuth(
p.appRole,
secretID,
)
if err != nil {
return errors.Wrap(errFailedAppRole, err)
}
if p.namespace != "" {
p.client.SetNamespace(p.namespace)
}
secret, err := p.client.Auth().Login(ctx, authMethod)
if err != nil {
return errors.Wrap(errFailedToLogin, err)
}
if secret == nil {
return errNoAuthInfo
}
p.secret = secret
return nil
}

func (p *pkiAgent) manageTokenLifecycle() error {
renew := p.secret.Auth.Renewable
if !renew {
return errNonRenewal
}

watcher, err := p.client.NewLifetimeWatcher(&api.LifetimeWatcherInput{
Secret: p.secret,
Increment: 3600, // Requesting token for 3600s = 1h, If this is more than token_max_ttl, then response token will have token_max_ttl
})
if err != nil {
return errors.Wrap(errRenewWatcher, err)
}

go watcher.Start()
defer watcher.Stop()

for {
select {
case err := <-watcher.DoneCh():
if err != nil {
return errors.Wrap(errFailedRenew, err)
}
// This occurs once the token has reached max TTL or if token is disabled for renewal.
return errCouldNotRenew

case renewal := <-watcher.RenewCh():
p.logger.Info("Successfully renewed token", slog.Any("renewed_at", renewal.RenewedAt))
}
}
}
16 changes: 11 additions & 5 deletions cmd/certs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ type config struct {
SignCAKeyPath string `env:"MG_CERTS_SIGN_CA_KEY_PATH" envDefault:"ca.key"`

// 3rd party PKI API access settings
PkiHost string `env:"MG_CERTS_VAULT_HOST" envDefault:""`
PkiPath string `env:"MG_VAULT_PKI_INT_PATH" envDefault:"pki_int"`
PkiRole string `env:"MG_VAULT_CA_ROLE_NAME" envDefault:"magistrala"`
PkiToken string `env:"MG_VAULT_TOKEN" envDefault:""`
PkiHost string `env:"MG_CERTS_VAULT_HOST" envDefault:""`
PkiAppRoleID string `env:"MG_CERTS_VAULT_APPROLE_ROLEID" envDefault:""`
PkiAppSecret string `env:"MG_CERTS_VAULT_APPROLE_SECRET" envDefault:""`
PkiNamespace string `env:"MG_CERTS_VAULT_NAMESPACE" envDefault:""`
PkiPath string `env:"MG_CERTS_VAULT_THINGS_CERTS_PKI_PATH" envDefault:"pki_int"`
PkiRole string `env:"MG_CERTS_VAULT_THINGS_CERTS_PKI_ROLE_NAME" envDefault:"magistrala"`
}

func main() {
Expand Down Expand Up @@ -94,13 +96,17 @@ func main() {
return
}

pkiclient, err := vault.NewVaultClient(cfg.PkiToken, cfg.PkiHost, cfg.PkiPath, cfg.PkiRole)
pkiclient, err := vault.NewVaultClient(cfg.PkiAppRoleID, cfg.PkiAppSecret, cfg.PkiHost, cfg.PkiNamespace, cfg.PkiPath, cfg.PkiRole, logger)
if err != nil {
logger.Error("failed to configure client for PKI engine")
exitCode = 1
return
}

g.Go(func() error {
return pkiclient.LoginAndRenew(ctx)
})

dbConfig := pgclient.Config{Name: defDB}
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
logger.Error(err.Error())
Expand Down
Loading

0 comments on commit ab4206c

Please sign in to comment.