Skip to content

Commit

Permalink
⭐ ssh key authentication (#100)
Browse files Browse the repository at this point in the history
Once the ssh public keys have been uploaded to Mondoo Platform, users
can authenticate with their ssh keys

```
api_endpoint: https://us.api.mondoo.com
auth:
  method: ssh
space_mrn: //captain.api.mondoo.app/{SPACEID}
mrn: //captain.api.mondoo.app/users/{USERID}
```

Signed-off-by: Christoph Hartmann <[email protected]>
  • Loading branch information
chris-rock authored Jan 12, 2023
1 parent 12aaa3f commit e887d6f
Show file tree
Hide file tree
Showing 13 changed files with 872 additions and 199 deletions.
43 changes: 30 additions & 13 deletions apps/cnquery/cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,20 @@ type CommonCliConfig struct {
Certificate string `json:"certificate,omitempty" mapstructure:"certificate"`
APIEndpoint string `json:"api_endpoint,omitempty" mapstructure:"api_endpoint"`

// authentication
Authentication *CliConfigAuthentication `json:"auth,omitempty" mapstructure:"auth"`

// client features
Features []string `json:"features,omitempty" mapstructure:"features"`

// labels that will be applied to all assets
Labels map[string]string `json:"labels,omitempty" mapstructure:"labels"`
}

type CliConfigAuthentication struct {
Method string `json:"method,omitempty" mapstructure:"method"`
}

func (c *CommonCliConfig) GetFeatures() cnquery.Features {
bitSet := make([]bool, 256)
flags := []byte{}
Expand Down Expand Up @@ -76,35 +83,45 @@ func (c *CommonCliConfig) GetFeatures() cnquery.Features {
}

// GetServiceCredential returns the service credential that is defined in the config.
// If no service credential is defined, it will return an nil.
func (v *CommonCliConfig) GetServiceCredential() *upstream.ServiceAccountCredentials {
// If no service credential is defined, it will return nil.
func (c *CommonCliConfig) GetServiceCredential() *upstream.ServiceAccountCredentials {
if c.Authentication != nil && c.Authentication.Method == "ssh" {
log.Info().Msg("using ssh authentication method, generate temporary credentials")
serviceAccount, err := upstream.ExchangeSSHKey(c.UpstreamApiEndpoint(), c.ServiceAccountMrn, c.GetParentMrn())
if err != nil {
log.Error().Err(err).Msg("could not exchange ssh key")
return nil
}
return serviceAccount
}

// return nil when no service account is defined
if v.ServiceAccountMrn == "" && v.PrivateKey == "" && v.Certificate == "" {
if c.ServiceAccountMrn == "" && c.PrivateKey == "" && c.Certificate == "" {
return nil
}

return &upstream.ServiceAccountCredentials{
Mrn: v.ServiceAccountMrn,
ParentMrn: v.GetParentMrn(),
PrivateKey: v.PrivateKey,
Certificate: v.Certificate,
ApiEndpoint: v.APIEndpoint,
Mrn: c.ServiceAccountMrn,
ParentMrn: c.GetParentMrn(),
PrivateKey: c.PrivateKey,
Certificate: c.Certificate,
ApiEndpoint: c.APIEndpoint,
}
}

func (o *CommonCliConfig) GetParentMrn() string {
parent := o.ParentMrn
func (c *CommonCliConfig) GetParentMrn() string {
parent := c.ParentMrn

// fallback to old space_mrn config
if parent == "" {
parent = o.SpaceMrn
parent = c.SpaceMrn
}

return parent
}

func (o *CommonCliConfig) UpstreamApiEndpoint() string {
apiEndpoint := o.APIEndpoint
func (c *CommonCliConfig) UpstreamApiEndpoint() string {
apiEndpoint := c.APIEndpoint

// fallback to default api if nothing was set
if apiEndpoint == "" {
Expand Down
8 changes: 5 additions & 3 deletions apps/cnquery/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ Status sends a ping to Mondoo Platform to verify the credentials.

// check valid agent authentication
plugins := []ranger.ClientPlugin{}
// plugins = append(plugins, max.NewClientInfoPlugin(clientInfo, opts.GetFeatures()))

// try to load config into credentials struct
credentials := opts.GetServiceCredential()
Expand All @@ -90,7 +89,10 @@ Status sends a ping to Mondoo Platform to verify the credentials.
s.Client.Mrn = "no managed client"
}

certAuth, _ := upstream.NewServiceAccountRangerPlugin(credentials)
certAuth, err := upstream.NewServiceAccountRangerPlugin(credentials)
if err != nil {
log.Fatal().Err(err).Msg("invalid credentials")
}
plugins = append(plugins, certAuth)

// try to ping the server
Expand Down Expand Up @@ -179,7 +181,7 @@ func (s Status) RenderCliStatus() {
if s.Client.Registered && s.Client.PingPongError == nil {
log.Info().Msg(theme.DefaultTheme.Success("client authenticated successfully"))
} else {
log.Error().Err(s.Client.PingPongError).Msg("could not connect to mondoo platform")
log.Error().Err(s.Client.PingPongError).Msg("could not connect to Mondoo Platform")
}

for i := range s.Upstream.Warnings {
Expand Down
66 changes: 33 additions & 33 deletions motor/inventory/v1/inventory.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion motor/inventory/v1/inventory.proto
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ message InventorySpec {
string credential_query = 4;

// optional: the upstream credentials to use for the inventory
mondoo.upstream.v1.ServiceAccountCredentials upstream_credentials = 16;
mondoo.cnquery.upstream.v1.ServiceAccountCredentials upstream_credentials = 16;
}

message InventoryStatus {}
Expand Down
77 changes: 15 additions & 62 deletions motor/providers/ssh/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ package ssh

import (
"context"
"encoding/pem"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/cockroachdb/errors"
"github.com/rs/zerolog/log"
"go.mondoo.com/cnquery/motor/providers"
"go.mondoo.com/cnquery/motor/providers/ssh/awsinstanceconnect"
"go.mondoo.com/cnquery/motor/providers/ssh/awsssmsession"
"go.mondoo.com/cnquery/motor/providers/ssh/signers"
"go.mondoo.com/cnquery/motor/vault"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
Expand Down Expand Up @@ -48,32 +45,6 @@ func establishClientConnection(pCfg *providers.Config, hostKeyCallback ssh.HostK
return conn, closer, err
}

func authPrivateKeyWithPassphrase(pemBytes []byte, passphrase []byte) (ssh.Signer, error) {
// check if the key is encrypted
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("ssh: no key found")
}

var signer ssh.Signer
var err error
if strings.Contains(block.Headers["Proc-Type"], "ENCRYPTED") {
// we may want to support to parse password protected encrypted key
signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, passphrase)
if err != nil {
return nil, err
}
} else {
// parse unencrypted key
signer, err = ssh.ParsePrivateKey(pemBytes)
if err != nil {
return nil, err
}
}

return signer, nil
}

// hasAgentLoadedKey returns if the ssh agent has loaded the key file
// This may not be 100% accurate. The key can be stored in multiple locations with the
// same fingerprint. We cannot determine the fingerprint without decoding the encrypted
Expand All @@ -95,25 +66,7 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
closer := []io.Closer{}

// only one public auth method is allowed, therefore multiple keys need to be encapsulated into one auth method
signers := []ssh.Signer{}

// enable ssh agent auth
useAgentAuth := func() {
if sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
log.Debug().Str("socket", os.Getenv("SSH_AUTH_SOCK")).Msg("ssh agent socket found")
sshAgentClient := agent.NewClient(sshAgentConn)
sshAgentSigners, err := sshAgentClient.Signers()
if err == nil && len(sshAgentSigners) == 0 {
log.Warn().Msg("could not find keys in ssh agent")
} else if err == nil {
signers = append(signers, sshAgentSigners...)
} else {
log.Error().Err(err).Msg("could not get public keys from ssh agent")
}
} else {
log.Debug().Msg("could not find valud ssh agent authentication")
}
}
sshSigners := []ssh.Signer{}

// use key auth, only load if the key was not found in ssh agent
for i := range pCfg.Credentials {
Expand All @@ -122,11 +75,11 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
switch credential.Type {
case vault.CredentialType_private_key:
log.Debug().Msg("enabled ssh private key authentication")
priv, err := authPrivateKeyWithPassphrase(credential.Secret, []byte(credential.Password))
priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(credential.Secret, []byte(credential.Password))
if err != nil {
log.Debug().Err(err).Msg("could not read private key")
} else {
signers = append(signers, priv)
sshSigners = append(sshSigners, priv)
}
case vault.CredentialType_password:
// use password auth if the password was set, this is also used when only the username is set
Expand All @@ -136,7 +89,7 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
}
case vault.CredentialType_ssh_agent:
log.Debug().Msg("enabled ssh agent authentication")
useAgentAuth()
sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
case vault.CredentialType_aws_ec2_ssm_session:
// when the user establishes the ssm session we do the following
// 1. start websocket connection and start the session-manager-plugin to map the websocket to a local port
Expand Down Expand Up @@ -204,11 +157,11 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
pCfg.Insecure = true

// use the generated ssh credentials for authentication
priv, err := authPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
if err != nil {
return nil, nil, errors.Wrap(err, "could not read generated private key")
}
signers = append(signers, priv)
sshSigners = append(sshSigners, priv)
closer = append(closer, ssmConn)
case vault.CredentialType_aws_ec2_instance_connect:
log.Debug().Str("profile", pCfg.Options["profile"]).Str("region", pCfg.Options["region"]).Msg("using aws creds")
Expand All @@ -235,11 +188,11 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
return nil, nil, err
}

priv, err := authPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase)
if err != nil {
return nil, nil, errors.Wrap(err, "could not read generated private key")
}
signers = append(signers, priv)
sshSigners = append(sshSigners, priv)

// NOTE: this creates a side-effect where the host is overwritten
pCfg.Host = creds.PublicIpAddress
Expand All @@ -248,14 +201,14 @@ func prepareConnection(pCfg *providers.Config) ([]ssh.AuthMethod, []io.Closer, e
}
}

// if no credential was provided, fallback to ssh-agent and ssh-config
if len(auths) == 0 {
log.Debug().Msg("enabled ssh agent authentication")
useAgentAuth()
if len(sshSigners) > 0 {
auths = append(auths, ssh.PublicKeys(sshSigners...))
}

if len(signers) > 0 {
auths = append(auths, ssh.PublicKeys(signers...))
// if no credential was provided, fallback to ssh-agent and ssh-config
if len(pCfg.Credentials) == 0 {
sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...)
}

return auths, closer, nil
}
Loading

0 comments on commit e887d6f

Please sign in to comment.