Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gmsa linux support #3464

Merged
merged 4 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ additional details on each available environment variable.
| `ECS_ENABLE_GPU_SUPPORT` | `true` | Whether you use container instances with GPU support. This parameter is specified for the agent. You must also configure your task definitions for GPU. For more information | `false` | `Not applicable` |
| `HTTP_PROXY` | `10.0.0.131:3128` | The hostname (or IP address) and port number of an HTTP proxy to use for the Amazon ECS agent to connect to the internet. For example, this proxy will be used if your container instances do not have external network access through an Amazon VPC internet gateway or NAT gateway or instance. If this variable is set, you must also set the NO_PROXY variable to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` |
| `NO_PROXY` | <For Linux: 169.254.169.254,169.254.170.2,/var/run/docker.sock &#124; For Windows: 169.254.169.254,169.254.170.2,\\.\pipe\docker_engine> | The HTTP traffic that should not be forwarded to the specified HTTP_PROXY. You must specify 169.254.169.254,/var/run/docker.sock to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` |

| `CREDENTIALS_FETCHER_HOST` | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Used to create a connection to the [credentials-fetcher daemon](https://github.com/aws/credentials-fetcher); to support gMSA on Linux. The default is fine for most users, only needs to be modified if user is configuring a custom credentials-fetcher socket path, ie, [CF_UNIX_DOMAIN_SOCKET_DIR](https://github.com/aws/credentials-fetcher#default-environment-variables). | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Not Applicable |
| `CREDENTIALS_FETCHER_SECRET_NAME_FOR_DOMAINLESS_GMSA` | `secretmanager-secretname` | Used to support scaling option for gMSA on Linux [credentials-fetcher daemon](https://github.com/aws/credentials-fetcher). If user is configuring gMSA on a non-domain joined instance, they need to create an Active Directory user with access to retrieve principals for the gMSA account and store it in secrets manager | `secretmanager-secretname` | Not Applicable |
### Persistence

When you run the Amazon ECS Container Agent in production, its `datadir` should be persisted between runs of the Docker
Expand Down
40 changes: 40 additions & 0 deletions agent/api/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ package container

import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -1325,6 +1327,44 @@ func (c *Container) UpdateManagedAgentSentStatus(agentName string, status apicon
return false
}

// RequiresCredentialSpec checks if container needs a credentialspec resource
func (c *Container) RequiresCredentialSpec() bool {
credSpec, err := c.getCredentialSpec()
if err != nil || credSpec == "" {
return false
}

return true
}

// GetCredentialSpec is used to retrieve the current credentialspec resource
func (c *Container) GetCredentialSpec() (string, error) {
return c.getCredentialSpec()
}

func (c *Container) getCredentialSpec() (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()

if c.DockerConfig.HostConfig == nil {
return "", errors.New("empty container hostConfig")
}

hostConfig := &dockercontainer.HostConfig{}
err := json.Unmarshal([]byte(*c.DockerConfig.HostConfig), hostConfig)
if err != nil || len(hostConfig.SecurityOpt) == 0 {
return "", errors.New("unable to obtain security options from container hostConfig")
}

for _, opt := range hostConfig.SecurityOpt {
if strings.HasPrefix(opt, "credentialspec") {
return opt, nil
}
}

return "", errors.New("unable to obtain credentialspec")
}

func (c *Container) GetManagedAgentStatus(agentName string) apicontainerstatus.ManagedAgentStatus {
c.lock.RLock()
defer c.lock.RUnlock()
Expand Down
110 changes: 110 additions & 0 deletions agent/api/container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,3 +970,113 @@ func TestUpdateManagedAgentSentStatus(t *testing.T) {
})
}
}

func TestRequiresCredentialSpec(t *testing.T) {
testCases := []struct {
name string
container *Container
expectedOutput bool
}{
{
name: "hostconfig_nil",
container: &Container{},
expectedOutput: false,
},
{
name: "invalid_case",
container: getContainer("invalid"),
expectedOutput: false,
},
{
name: "empty_sec_opt",
container: getContainer("{\"NetworkMode\":\"bridge\"}"),
expectedOutput: false,
},
{
name: "missing_credentialspec",
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"),
expectedOutput: false,
},
{
name: "valid_credentialspec_file",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}"),
expectedOutput: true,
},
{
name: "valid_credentialspec_s3",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}"),
expectedOutput: true,
},
{
name: "valid_credentialspec_ssm",
container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}"),
expectedOutput: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expectedOutput, tc.container.RequiresCredentialSpec())
})
}
}

func TestGetCredentialSpecErr(t *testing.T) {
testCases := []struct {
name string
container *Container
expectedOutputString string
expectedErrorString string
}{
{
name: "hostconfig_nil",
container: &Container{},
expectedOutputString: "",
expectedErrorString: "empty container hostConfig",
},
{
name: "invalid_case",
container: getContainer("invalid"),
expectedOutputString: "",
expectedErrorString: "unable to obtain security options from container hostConfig",
},
{
name: "empty_sec_opt",
container: getContainer("{\"NetworkMode\":\"bridge\"}"),
expectedOutputString: "",
expectedErrorString: "unable to obtain security options from container hostConfig",
},
{
name: "missing_credentialspec",
container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"),
expectedOutputString: "",
expectedErrorString: "unable to obtain credentialspec",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedOutputStr, err := tc.container.GetCredentialSpec()
assert.Equal(t, tc.expectedOutputString, expectedOutputStr)
assert.EqualError(t, err, tc.expectedErrorString)
})
}
}

func TestGetCredentialSpecHappyPath(t *testing.T) {
c := getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}")

expectedCredentialSpec := "credentialspec:file://gmsa_gmsa-acct.json"

credentialspec, err := c.GetCredentialSpec()
assert.NoError(t, err)
assert.EqualValues(t, expectedCredentialSpec, credentialspec)
}

func getContainer(hostConfig string) *Container {
c := &Container{
Name: "c",
}
c.DockerConfig.HostConfig = &hostConfig
return c
}
14 changes: 0 additions & 14 deletions agent/api/container/container_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,8 @@

package container

import (
"github.com/pkg/errors"
)

const (
// DockerContainerMinimumMemoryInBytes is the minimum amount of
// memory to be allocated to a docker container
DockerContainerMinimumMemoryInBytes = 4 * 1024 * 1024 // 4MB
)

// RequiresCredentialSpec checks if container needs a credentialspec resource
func (c *Container) RequiresCredentialSpec() bool {
return false
}

// GetCredentialSpec is used to retrieve the current credentialspec resource
func (c *Container) GetCredentialSpec() (string, error) {
return "", errors.New("unsupported platform")
}
46 changes: 0 additions & 46 deletions agent/api/container/container_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,8 @@

package container

import (
"encoding/json"
"strings"

dockercontainer "github.com/docker/docker/api/types/container"
"github.com/pkg/errors"
)

const (
// DockerContainerMinimumMemoryInBytes is the minimum amount of
// memory to be allocated to a docker container
DockerContainerMinimumMemoryInBytes = 256 * 1024 * 1024 // 256MB
)

// RequiresCredentialSpec checks if container needs a credentialspec resource
func (c *Container) RequiresCredentialSpec() bool {
credSpec, err := c.getCredentialSpec()
if err != nil || credSpec == "" {
return false
}

return true
}

// GetCredentialSpec is used to retrieve the current credentialspec resource
func (c *Container) GetCredentialSpec() (string, error) {
return c.getCredentialSpec()
}

func (c *Container) getCredentialSpec() (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()

if c.DockerConfig.HostConfig == nil {
return "", errors.New("empty container hostConfig")
}

hostConfig := &dockercontainer.HostConfig{}
err := json.Unmarshal([]byte(*c.DockerConfig.HostConfig), hostConfig)
if err != nil || len(hostConfig.SecurityOpt) == 0 {
return "", errors.New("unable to obtain security options from container hostConfig")
}

for _, opt := range hostConfig.SecurityOpt {
if strings.HasPrefix(opt, "credentialspec") {
return opt, nil
}
}

return "", errors.New("unable to obtain credentialspec")
}
133 changes: 0 additions & 133 deletions agent/api/container/container_windows_test.go

This file was deleted.

Loading