Skip to content

Commit

Permalink
feat(cli): find and fix AWS instances w/o agents (#934)
Browse files Browse the repository at this point in the history
This commit is the first milestone for Project Capture The Flag. Project
CTF takes a user's cloud credentials, lists all of their hosts,
and installs the agent on hosts without agents. The purpose of
Project CTF is to allow customers to easily close gaps in their agent
deployments by using the CLI to find and install the agent on instances
that the customer would have trouble reaching otherwise.

This commit adds a new subcommand to the Lacework CLI called aws-install.
The user should invoke this command via lacework agent aws-install.
aws-install is currently a no-op and is used as a directory for further
subcommands.

This commit adds two subcommands to aws-install called ec2ic and ec2-ssh.
The user should invoke them as either lacework agent ctf aws-install ec2ic or
lacework agent ctf aws-install ec2-ssh.

The ec2ic subcommand uses EC2InstanceConnect to send SSH keys to the
user's EC2 instances. EC2InstanceConnect is only supported on official
channel AMIs that are Ubuntu 16.04 or later or Amazon Linux 2, so the
customer will not be able to use ec2ic for instances running other
AMIs. ec2ic adds flags to filter instances on tag, tag key, and region.

The ec2-ssh subcommand uses existing SSH authentication to install agents
on the user's EC2 instances. ec2-ssh assumes that all instances that it
finds will have SSH access via the authentication method passed via CLI
flags. ec2-ssh also adds flags to filter instances on tag, tag key, and
region.

ec2ic and ec2-ssh both have the limitation that they only work for
instances with public IP addresses that are open to the internet on port
22.

Fixes RAIN-37375
Fixes RAIN-38278

Signed-off-by: nschmeller <[email protected]>
Co-authored-by: Salim Afiune <[email protected]>
Co-authored-by: Chris Golden <[email protected]>
  • Loading branch information
3 people committed Oct 12, 2022
1 parent 76e6cc8 commit 4e507c0
Show file tree
Hide file tree
Showing 999 changed files with 456,067 additions and 11 deletions.
36 changes: 25 additions & 11 deletions cli/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@ import (

var (
agentCmdState = struct {
TokenUpdateEnable bool
TokenUpdateDisable bool
TokenUpdateName string
TokenUpdateDesc string
InstallForce bool
InstallSshUser string
InstallSshPort int
InstallAgentToken string
InstallTrustHostKey bool
InstallPassword string
InstallIdentityFile string
TokenUpdateEnable bool
TokenUpdateDisable bool
TokenUpdateName string
TokenUpdateDesc string
InstallForce bool
InstallSshUser string
InstallSshPort int
InstallAgentToken string
InstallTrustHostKey bool
InstallPassword string
InstallIdentityFile string
InstallTagKey string
InstallTag []string
InstallIncludeRegions []string
}{}

defaultSshIdentityKey = "~/.ssh/id_rsa"
Expand Down Expand Up @@ -158,6 +161,12 @@ To list all active agents in your environment.
NOTE: New agents could take up to an hour to report back to the platform.`,
RunE: installRemoteAgent,
}

agentAWSInstallCmd = &cobra.Command{
Use: "aws-install",
Args: cobra.NoArgs,
Short: "Install the datacollector agent on all remote AWS hosts",
}
)

func init() {
Expand All @@ -169,13 +178,18 @@ func init() {
agentCmd.AddCommand(agentInstallCmd)
agentCmd.AddCommand(agentGenerateCmd)
agentCmd.AddCommand(agentListCmd)
agentCmd.AddCommand(agentAWSInstallCmd)

// add the list sub-command to the 'agent token' cmd
agentTokenCmd.AddCommand(agentTokenListCmd)
agentTokenCmd.AddCommand(agentTokenCreateCmd)
agentTokenCmd.AddCommand(agentTokenShowCmd)
agentTokenCmd.AddCommand(agentTokenUpdateCmd)

// add sub-commands to the 'agent aws-install' command for different install methods
agentAWSInstallCmd.AddCommand(agentInstallAWSEC2ICCmd)
agentAWSInstallCmd.AddCommand(agentInstallAWSSSHCmd)

// 'agent token update' flags
agentTokenUpdateCmd.Flags().BoolVar(&agentCmdState.TokenUpdateEnable,
"enable", false, "enable agent access token",
Expand Down
127 changes: 127 additions & 0 deletions cli/cmd/agent_install_ec2ic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// Author:: Nicholas Schmeller (<[email protected]>)
// Copyright:: Copyright 2022, Lacework Inc.
// License:: Apache License, Version 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package cmd

import (
"fmt"

"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var (
agentInstallAWSEC2ICCmd = &cobra.Command{
Use: "ec2ic",
Args: cobra.NoArgs,
Short: "Use EC2InstanceConnect to securely connect to EC2 instances",
RunE: installAWSEC2IC,
Long: `This command installs the agent on all EC2 instances in an AWS account using EC2InstanceConnect.
To filter by one or more regions:
lacework agent install ec2ic --include_regions us-west-2,us-east-2
To filter by instance tag:
lacework agent install ec2ic --tag TagName,TagValue
To filter by instance tag key:
lacework agent install ec2ic --tag_key TagName
To explicitly specify the username for all SSH logins:
lacework agent install ec2ic --ssh_username <your-user>
AWS credentials are read from the following environment variables:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_SESSION_TOKEN (optional)
- AWS_REGION (optional)
This command will automatically add hosts with successful connections to
'~/.ssh/known_hosts' unless specified with '--trust_host_key=false'.`,
}
)

func init() {
// 'agent install ec2ic' flags
agentInstallAWSEC2ICCmd.Flags().StringVar(&agentCmdState.InstallTagKey,
"tag_key", "", "only install agents on infra with this tag key set",
)
agentInstallAWSEC2ICCmd.Flags().StringSliceVar(&agentCmdState.InstallTag,
"tag", []string{}, "only install agents on infra with this tag",
)
agentInstallAWSEC2ICCmd.Flags().StringVar(&agentCmdState.InstallAgentToken,
"token", "", "agent access token",
)
agentInstallAWSEC2ICCmd.Flags().BoolVar(&agentCmdState.InstallTrustHostKey,
"trust_host_key", true, "automatically add host keys to the ~/.ssh/known_hosts file",
)
agentInstallAWSEC2ICCmd.Flags().StringSliceVarP(&agentCmdState.InstallIncludeRegions,
"include_regions", "r", []string{}, "list of regions to filter on",
)
agentInstallAWSEC2ICCmd.Flags().StringVar(&agentCmdState.InstallSshUser,
"ssh_username", "", "username to login with",
)
}

func installAWSEC2IC(_ *cobra.Command, _ []string) error {
runners, err := awsDescribeInstances()
if err != nil {
return err
}

for _, runner := range runners {
cli.Log.Debugw("runner info: ", "user", runner.Runner.User, "region", runner.Region, "az", runner.AvailabilityZone, "instance ID", runner.InstanceID, "hostname", runner.Runner.Hostname)
err := runner.SendAndUseIdentityFile()
if err != nil {
cli.Log.Debugw("ec2ic failed", "err", err)
continue
}

if err := verifyAccessToRemoteHost(&runner.Runner); err != nil {
cli.Log.Debugw("verifyAccessToRemoteHost failed")
return err
}

if alreadyInstalled := isAgentInstalledOnRemoteHost(&runner.Runner); alreadyInstalled != nil {
cli.Log.Debugw("agent already installed on host, skipping")
continue
}

token := agentCmdState.InstallAgentToken
if token == "" {
// user didn't provide an agent token
cli.Log.Debugw("agent token not provided")
var err error
token, err = selectAgentAccessToken()
if err != nil {
return err
}
}
cmd := fmt.Sprintf("sudo sh -c \"curl -sSL %s | sh -s -- %s\"", agentInstallDownloadURL, token)
err = runInstallCommandOnRemoteHost(&runner.Runner, cmd)
if err != nil {
return errors.Wrap(err, "runInstallCommandOnRemoteHost failed for instance "+runner.InstanceID)
}
}

return nil
}
71 changes: 71 additions & 0 deletions cli/cmd/agent_install_ec2ic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// Author:: Nicholas Schmeller (<[email protected]>)
// Copyright:: Copyright 2022, Lacework Inc.
// License:: Apache License, Version 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package cmd

import (
"fmt"
"os"
"sync"
"testing"

"github.com/lacework/go-sdk/lwrunner"
"github.com/stretchr/testify/assert"
)

// Requires AWS credentials in the shell environment
// Lists runners, sends keys, attempts to connect
// Example command to run:
// `aws-vault exec default -- go test -run TestAwsEC2ICDescribeInstances`
// If AWS credentials are already present in the shell environment, only use:
// `go test -run TestAwsEC2ICDescribeInstances`
func TestAwsEC2ICDescribeInstances(t *testing.T) {
if _, ok := os.LookupEnv("AWS_SECRET_ACCESS_KEY"); !ok {
t.Skip("aws credentials not found in environment, skipping test")
}

cli.LogLevel = "DEBUG"
agentCmdState.InstallTrustHostKey = true
agentCmdState.InstallTagKey = "CaptureTheFlagPlayer"
cli.NonInteractive()

runners, err := awsDescribeInstances()
assert.NoError(t, err)

wg := new(sync.WaitGroup)
for _, runner := range runners {
wg.Add(1)
go func(runner *lwrunner.AWSRunner) {
out := fmt.Sprintf("--------- Runner ---------\nRegion: %v\nInstance ID: %v\n", runner.Region, runner.InstanceID)

err = runner.SendAndUseIdentityFile()
assert.NoError(t, err)

err = verifyAccessToRemoteHost(&runner.Runner)
assert.NoError(t, err)

if alreadyInstalled := isAgentInstalledOnRemoteHost(&runner.Runner); alreadyInstalled != nil {
out += alreadyInstalled.Error() + "\n"
}

fmt.Println(out)
wg.Done()
}(runner)
}
wg.Wait()
}
Loading

0 comments on commit 4e507c0

Please sign in to comment.