277 lines
8.1 KiB
Go
277 lines
8.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
rootcerts "github.com/hashicorp/go-rootcerts"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
const (
|
|
// SSHHelperDefaultMountPoint is the default path at which SSH backend will be
|
|
// mounted in the Vault server.
|
|
SSHHelperDefaultMountPoint = "ssh"
|
|
|
|
// VerifyEchoRequest is the echo request message sent as OTP by the helper.
|
|
VerifyEchoRequest = "verify-echo-request"
|
|
|
|
// VerifyEchoResponse is the echo response message sent as a response to OTP
|
|
// matching echo request.
|
|
VerifyEchoResponse = "verify-echo-response"
|
|
)
|
|
|
|
// SSHHelper is a structure representing a vault-ssh-helper which can talk to vault server
|
|
// in order to verify the OTP entered by the user. It contains the path at which
|
|
// SSH backend is mounted at the server.
|
|
type SSHHelper struct {
|
|
c *Client
|
|
MountPoint string
|
|
}
|
|
|
|
// SSHVerifyResponse is a structure representing the fields in Vault server's
|
|
// response.
|
|
type SSHVerifyResponse struct {
|
|
// Usually empty. If the request OTP is echo request message, this will
|
|
// be set to the corresponding echo response message.
|
|
Message string `json:"message" mapstructure:"message"`
|
|
|
|
// Username associated with the OTP
|
|
Username string `json:"username" mapstructure:"username"`
|
|
|
|
// IP associated with the OTP
|
|
IP string `json:"ip" mapstructure:"ip"`
|
|
|
|
// Name of the role against which the OTP was issued
|
|
RoleName string `json:"role_name" mapstructure:"role_name"`
|
|
}
|
|
|
|
// SSHHelperConfig is a structure which represents the entries from the vault-ssh-helper's configuration file.
|
|
type SSHHelperConfig struct {
|
|
VaultAddr string `hcl:"vault_addr"`
|
|
SSHMountPoint string `hcl:"ssh_mount_point"`
|
|
Namespace string `hcl:"namespace"`
|
|
CACert string `hcl:"ca_cert"`
|
|
CAPath string `hcl:"ca_path"`
|
|
AllowedCidrList string `hcl:"allowed_cidr_list"`
|
|
AllowedRoles string `hcl:"allowed_roles"`
|
|
TLSSkipVerify bool `hcl:"tls_skip_verify"`
|
|
TLSServerName string `hcl:"tls_server_name"`
|
|
}
|
|
|
|
// SetTLSParameters sets the TLS parameters for this SSH agent.
|
|
func (c *SSHHelperConfig) SetTLSParameters(clientConfig *Config, certPool *x509.CertPool) {
|
|
tlsConfig := &tls.Config{
|
|
InsecureSkipVerify: c.TLSSkipVerify,
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: certPool,
|
|
ServerName: c.TLSServerName,
|
|
}
|
|
|
|
transport := cleanhttp.DefaultTransport()
|
|
transport.TLSClientConfig = tlsConfig
|
|
clientConfig.HttpClient.Transport = transport
|
|
}
|
|
|
|
// Returns true if any of the following conditions are true:
|
|
// - CA cert is configured
|
|
// - CA path is configured
|
|
// - configured to skip certificate verification
|
|
// - TLS server name is configured
|
|
func (c *SSHHelperConfig) shouldSetTLSParameters() bool {
|
|
return c.CACert != "" || c.CAPath != "" || c.TLSServerName != "" || c.TLSSkipVerify
|
|
}
|
|
|
|
// NewClient returns a new client for the configuration. This client will be used by the
|
|
// vault-ssh-helper to communicate with Vault server and verify the OTP entered by user.
|
|
// If the configuration supplies Vault SSL certificates, then the client will
|
|
// have TLS configured in its transport.
|
|
func (c *SSHHelperConfig) NewClient() (*Client, error) {
|
|
// Creating a default client configuration for communicating with vault server.
|
|
clientConfig := DefaultConfig()
|
|
|
|
// Pointing the client to the actual address of vault server.
|
|
clientConfig.Address = c.VaultAddr
|
|
|
|
// Check if certificates are provided via config file.
|
|
if c.shouldSetTLSParameters() {
|
|
rootConfig := &rootcerts.Config{
|
|
CAFile: c.CACert,
|
|
CAPath: c.CAPath,
|
|
}
|
|
certPool, err := rootcerts.LoadCACerts(rootConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Enable TLS on the HTTP client information
|
|
c.SetTLSParameters(clientConfig, certPool)
|
|
}
|
|
|
|
// Creating the client object for the given configuration
|
|
client, err := NewClient(clientConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Configure namespace
|
|
if c.Namespace != "" {
|
|
client.SetNamespace(c.Namespace)
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// LoadSSHHelperConfig loads ssh-helper's configuration from the file and populates the corresponding
|
|
// in-memory structure.
|
|
//
|
|
// Vault address is a required parameter.
|
|
// Mount point defaults to "ssh".
|
|
func LoadSSHHelperConfig(path string) (*SSHHelperConfig, error) {
|
|
contents, err := ioutil.ReadFile(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, multierror.Prefix(err, "ssh_helper:")
|
|
}
|
|
return ParseSSHHelperConfig(string(contents))
|
|
}
|
|
|
|
// ParseSSHHelperConfig parses the given contents as a string for the SSHHelper
|
|
// configuration.
|
|
func ParseSSHHelperConfig(contents string) (*SSHHelperConfig, error) {
|
|
root, err := hcl.Parse(string(contents))
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf("error parsing config: {{err}}", err)
|
|
}
|
|
|
|
list, ok := root.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing config: file doesn't contain a root object")
|
|
}
|
|
|
|
valid := []string{
|
|
"vault_addr",
|
|
"ssh_mount_point",
|
|
"namespace",
|
|
"ca_cert",
|
|
"ca_path",
|
|
"allowed_cidr_list",
|
|
"allowed_roles",
|
|
"tls_skip_verify",
|
|
"tls_server_name",
|
|
}
|
|
if err := CheckHCLKeys(list, valid); err != nil {
|
|
return nil, multierror.Prefix(err, "ssh_helper:")
|
|
}
|
|
|
|
var c SSHHelperConfig
|
|
c.SSHMountPoint = SSHHelperDefaultMountPoint
|
|
if err := hcl.DecodeObject(&c, list); err != nil {
|
|
return nil, multierror.Prefix(err, "ssh_helper:")
|
|
}
|
|
|
|
if c.VaultAddr == "" {
|
|
return nil, fmt.Errorf(`missing config "vault_addr"`)
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func CheckHCLKeys(node ast.Node, valid []string) error {
|
|
var list *ast.ObjectList
|
|
switch n := node.(type) {
|
|
case *ast.ObjectList:
|
|
list = n
|
|
case *ast.ObjectType:
|
|
list = n.List
|
|
default:
|
|
return fmt.Errorf("cannot check HCL keys of type %T", n)
|
|
}
|
|
|
|
validMap := make(map[string]struct{}, len(valid))
|
|
for _, v := range valid {
|
|
validMap[v] = struct{}{}
|
|
}
|
|
|
|
var result error
|
|
for _, item := range list.Items {
|
|
key := item.Keys[0].Token.Value().(string)
|
|
if _, ok := validMap[key]; !ok {
|
|
result = multierror.Append(result, fmt.Errorf("invalid key %q on line %d", key, item.Assign.Line))
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// SSHHelper creates an SSHHelper object which can talk to Vault server with SSH backend
|
|
// mounted at default path ("ssh").
|
|
func (c *Client) SSHHelper() *SSHHelper {
|
|
return c.SSHHelperWithMountPoint(SSHHelperDefaultMountPoint)
|
|
}
|
|
|
|
// SSHHelperWithMountPoint creates an SSHHelper object which can talk to Vault server with SSH backend
|
|
// mounted at a specific mount point.
|
|
func (c *Client) SSHHelperWithMountPoint(mountPoint string) *SSHHelper {
|
|
return &SSHHelper{
|
|
c: c,
|
|
MountPoint: mountPoint,
|
|
}
|
|
}
|
|
|
|
// Verify verifies if the key provided by user is present in Vault server. The response
|
|
// will contain the IP address and username associated with the OTP. In case the
|
|
// OTP matches the echo request message, instead of searching an entry for the OTP,
|
|
// an echo response message is returned. This feature is used by ssh-helper to verify if
|
|
// its configured correctly.
|
|
func (c *SSHHelper) Verify(otp string) (*SSHVerifyResponse, error) {
|
|
return c.VerifyWithContext(context.Background(), otp)
|
|
}
|
|
|
|
// VerifyWithContext the same as Verify but with a custom context.
|
|
func (c *SSHHelper) VerifyWithContext(ctx context.Context, otp string) (*SSHVerifyResponse, error) {
|
|
ctx, cancelFunc := c.c.withConfiguredTimeout(ctx)
|
|
defer cancelFunc()
|
|
|
|
data := map[string]interface{}{
|
|
"otp": otp,
|
|
}
|
|
verifyPath := fmt.Sprintf("/v1/%s/verify", c.MountPoint)
|
|
r := c.c.NewRequest(http.MethodPut, verifyPath)
|
|
if err := r.SetJSONBody(data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := c.c.rawRequestWithContext(ctx, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
secret, err := ParseSecret(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if secret.Data == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var verifyResp SSHVerifyResponse
|
|
err = mapstructure.Decode(secret.Data, &verifyResp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &verifyResp, nil
|
|
}
|