d0e2badbae
The result will still pass gofmtcheck and won't trigger additional changes if someone isn't using goimports, but it will avoid the piecemeal imports changes we've been seeing.
235 lines
7.1 KiB
Go
235 lines
7.1 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"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/hashicorp/vault/helper/hclutil"
|
|
"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"`
|
|
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
|
|
}
|
|
|
|
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",
|
|
"ca_cert",
|
|
"ca_path",
|
|
"allowed_cidr_list",
|
|
"allowed_roles",
|
|
"tls_skip_verify",
|
|
"tls_server_name",
|
|
}
|
|
if err := hclutil.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
|
|
}
|
|
|
|
// 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) {
|
|
data := map[string]interface{}{
|
|
"otp": otp,
|
|
}
|
|
verifyPath := fmt.Sprintf("/v1/%s/verify", c.MountPoint)
|
|
r := c.c.NewRequest("PUT", verifyPath)
|
|
if err := r.SetJSONBody(data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
defer cancelFunc()
|
|
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
|
|
}
|