use an initContainer to inject secrets as /run/secrets/xx

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-08-04 18:04:06 +02:00
parent b05af0c0ac
commit 85b3cbd6ea
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
10 changed files with 296 additions and 59 deletions

View File

@ -7,6 +7,8 @@ import (
"os"
"strings"
"github.com/docker/ecs-plugin/pkg/amazon/cloudformation"
"github.com/compose-spec/compose-go/cli"
"github.com/docker/cli/cli/command"
amazon "github.com/docker/ecs-plugin/pkg/amazon/backend"
@ -59,11 +61,11 @@ func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Comma
return err
}
j, err := template.JSON()
json, err := cloudformation.Marshall(template)
if err != nil {
fmt.Printf("Failed to generate JSON: %s\n", err)
} else {
fmt.Printf("%s\n", string(j))
fmt.Printf("%s\n", string(json))
}
return nil
}),

View File

@ -19,12 +19,8 @@ github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkK
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg=
github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.33.18 h1:Ccy1SV2SsgJU3rfrD+SOhQ0jvuzfrFuja/oKI86ruPw=
github.com/aws/aws-sdk-go v1.33.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc=
github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA=
github.com/awslabs/goformation/v4 v4.14.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -58,14 +54,6 @@ github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiK
github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8=
github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo=
github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6UpRqxROso9KC+ceaTiXx/VWpeO1x+NV0d4d+o=
github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5jML7rQ1aupg/KHlTqCxhyXvIgeDMf4kDTzIg8=
github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0=
github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3 h1:+ntlMTrEcScJjlnEOP8P1IIrusJaR93Eazr66YgUueA=
github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
@ -80,6 +68,7 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -475,6 +464,7 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ=
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI=

View File

@ -2,9 +2,12 @@ package backend
import (
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/elbv2"
cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
@ -93,6 +96,30 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro
networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(ParameterVPCId), template)
}
for i, s := range project.Secrets {
if s.External.External {
continue
}
secret, err := ioutil.ReadFile(s.File)
if err != nil {
return nil, err
}
name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
template.Resources[name] = &secretsmanager.Secret{
Description: "",
SecretString: string(secret),
Tags: []tags.Tag{
{
Key: compose.ProjectTag,
Value: project.Name,
},
},
}
s.Name = cloudformation.Ref(name)
project.Secrets[i] = s
}
logGroup := fmt.Sprintf("/docker-compose/%s", project.Name)
template.Resources["LogGroup"] = &logs.LogGroup{
LogGroupName: logGroup,

View File

@ -37,8 +37,14 @@ var compatibleComposeAttributes = []string{
"services.ports.mode",
"services.ports.target",
"services.ports.protocol",
"services.secrets",
"services.secrets.source",
"services.secrets.target",
"services.user",
"services.working_dir",
"secrets.external",
"secrets.name",
"secrets.file",
}
func (c *FargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) {

View File

@ -17,6 +17,8 @@ import (
"github.com/docker/ecs-plugin/pkg/compose"
)
const secretsInitContainerImage = "docker/ecs-secrets-sidecar"
func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
cpu, mem, err := toLimits(service)
if err != nil {
@ -37,50 +39,118 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
fmt.Sprintf(" %s.local", project.Name),
}))
return &ecs.TaskDefinition{
ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{
{
Command: service.Command,
DisableNetworking: service.NetworkMode == "none",
DnsSearchDomains: service.DNSSearch,
DnsServers: service.DNS,
DockerSecurityOptions: service.SecurityOpt,
EntryPoint: service.Entrypoint,
Environment: toKeyValuePair(service.Environment),
Essential: true,
ExtraHosts: toHostEntryPtr(service.ExtraHosts),
FirelensConfiguration: nil,
HealthCheck: toHealthCheck(service.HealthCheck),
Hostname: service.Hostname,
Image: service.Image,
Interactive: false,
Links: nil,
LinuxParameters: toLinuxParameters(service),
LogConfiguration: &ecs.TaskDefinition_LogConfiguration{
LogDriver: ecsapi.LogDriverAwslogs,
Options: map[string]string{
"awslogs-region": cloudformation.Ref("AWS::Region"),
"awslogs-group": cloudformation.Ref("LogGroup"),
"awslogs-stream-prefix": project.Name,
},
},
MemoryReservation: memReservation,
Name: service.Name,
PortMappings: toPortMappings(service.Ports),
Privileged: service.Privileged,
PseudoTerminal: service.Tty,
ReadonlyRootFilesystem: service.ReadOnly,
RepositoryCredentials: credential,
ResourceRequirements: nil,
StartTimeout: 0,
StopTimeout: durationToInt(service.StopGracePeriod),
SystemControls: toSystemControls(service.Sysctls),
Ulimits: toUlimits(service.Ulimits),
User: service.User,
VolumesFrom: nil,
WorkingDirectory: service.WorkingDir,
},
logConfiguration := &ecs.TaskDefinition_LogConfiguration{
LogDriver: ecsapi.LogDriverAwslogs,
Options: map[string]string{
"awslogs-region": cloudformation.Ref("AWS::Region"),
"awslogs-group": cloudformation.Ref("LogGroup"),
"awslogs-stream-prefix": project.Name,
},
}
var (
containers []ecs.TaskDefinition_ContainerDefinition
volumes []ecs.TaskDefinition_Volume
mounts []ecs.TaskDefinition_MountPoint
initContainers []ecs.TaskDefinition_ContainerDependency
)
if len(service.Secrets) > 0 {
volumes = append(volumes, ecs.TaskDefinition_Volume{
Name: "secrets",
})
mounts = append(mounts, ecs.TaskDefinition_MountPoint{
ContainerPath: "/run/secrets/",
ReadOnly: true,
SourceVolume: "secrets",
})
initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{
Condition: ecsapi.ContainerConditionSuccess,
ContainerName: "Secrets_InitContainer",
})
var (
names []string
secrets []ecs.TaskDefinition_Secret
)
for _, s := range service.Secrets {
secretConfig := project.Secrets[s.Source]
if s.Target == "" {
s.Target = s.Source
}
secrets = append(secrets, ecs.TaskDefinition_Secret{
Name: s.Target,
ValueFrom: secretConfig.Name,
})
name := s.Target
if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok {
var keys []string
if key, ok := ext.(string); ok {
keys = append(keys, key)
} else {
for _, k := range ext.([]interface{}) {
keys = append(keys, k.(string))
}
}
name = fmt.Sprintf("%s:%s", s.Target, strings.Join(keys, ","))
}
names = append(names, name)
}
containers = append(containers, ecs.TaskDefinition_ContainerDefinition{
Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)),
Image: secretsInitContainerImage,
Command: names,
Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607
LogConfiguration: logConfiguration,
MountPoints: []ecs.TaskDefinition_MountPoint{
{
ContainerPath: "/run/secrets/",
ReadOnly: false,
SourceVolume: "secrets",
},
},
Secrets: secrets,
})
}
containers = append(containers, ecs.TaskDefinition_ContainerDefinition{
Command: service.Command,
DisableNetworking: service.NetworkMode == "none",
DependsOnProp: initContainers,
DnsSearchDomains: service.DNSSearch,
DnsServers: service.DNS,
DockerSecurityOptions: service.SecurityOpt,
EntryPoint: service.Entrypoint,
Environment: toKeyValuePair(service.Environment),
Essential: true,
ExtraHosts: toHostEntryPtr(service.ExtraHosts),
FirelensConfiguration: nil,
HealthCheck: toHealthCheck(service.HealthCheck),
Hostname: service.Hostname,
Image: service.Image,
Interactive: false,
Links: nil,
LinuxParameters: toLinuxParameters(service),
LogConfiguration: logConfiguration,
MemoryReservation: memReservation,
MountPoints: mounts,
Name: service.Name,
PortMappings: toPortMappings(service.Ports),
Privileged: service.Privileged,
PseudoTerminal: service.Tty,
ReadonlyRootFilesystem: service.ReadOnly,
RepositoryCredentials: credential,
ResourceRequirements: nil,
StartTimeout: 0,
StopTimeout: durationToInt(service.StopGracePeriod),
SystemControls: toSystemControls(service.Sysctls),
Ulimits: toUlimits(service.Ulimits),
User: service.User,
VolumesFrom: nil,
WorkingDirectory: service.WorkingDir,
})
return &ecs.TaskDefinition{
ContainerDefinitions: containers,
Cpu: cpu,
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
IpcMode: service.Ipc,
@ -90,6 +160,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
PlacementConstraints: toPlacementConstraints(service.Deploy),
ProxyConfiguration: nil,
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
Volumes: volumes,
}, nil
}

View File

@ -0,0 +1,45 @@
package cloudformation
import (
"encoding/json"
"fmt"
"strings"
"github.com/awslabs/goformation/v4/cloudformation"
)
func Marshall(template *cloudformation.Template) ([]byte, error) {
raw, err := template.JSON()
if err != nil {
return nil, err
}
var unmarshalled interface{}
if err := json.Unmarshal(raw, &unmarshalled); err != nil {
return nil, fmt.Errorf("invalid JSON: %s", err)
}
if input, ok := unmarshalled.(map[string]interface{}); ok {
if resources, ok := input["Resources"]; ok {
for _, uresource := range resources.(map[string]interface{}) {
if resource, ok := uresource.(map[string]interface{}); ok {
if resource["Type"] == "AWS::ECS::TaskDefinition" {
properties := resource["Properties"].(map[string]interface{})
for _, def := range properties["ContainerDefinitions"].([]interface{}) {
containerDefinition := def.(map[string]interface{})
if strings.HasSuffix(containerDefinition["Name"].(string), "_InitContainer") {
containerDefinition["Essential"] = "false"
}
}
}
}
}
}
}
raw, err = json.MarshalIndent(unmarshalled, "", " ")
if err != nil {
return nil, fmt.Errorf("invalid JSON: %s", err)
}
return raw, err
}

View File

@ -6,6 +6,8 @@ import (
"strings"
"time"
cloudformation2 "github.com/docker/ecs-plugin/pkg/amazon/cloudformation"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
@ -164,7 +166,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error {
logrus.Debug("Create CloudFormation stack")
json, err := template.JSON()
json, err := cloudformation2.Marshall(template)
if err != nil {
return err
}
@ -192,7 +194,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template
func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) {
logrus.Debug("Create CloudFormation Changeset")
json, err := template.JSON()
json, err := cloudformation2.Marshall(template)
if err != nil {
return "", err
}

View File

@ -6,4 +6,5 @@ const (
ExtensionPullCredentials = "x-aws-pull_credentials"
ExtensionLB = "x-aws-loadbalancer"
ExtensionCluster = "x-aws-cluster"
ExtensionKeys = "x-aws-keys"
)

8
ecs/secrets/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM golang:1.14.4-alpine AS builder
WORKDIR $GOPATH/src/github.com/docker/ecs-secrets
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets
FROM scratch
COPY --from=builder /go/bin/secrets /secrets
ENTRYPOINT ["/secrets"]

85
ecs/secrets/main.go Normal file
View File

@ -0,0 +1,85 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// return codes:
// 1: failed to read secret from env
// 2: failed to parse hierarchical secret
// 3: failed to write secret content into file
func main() {
for _, name := range os.Args[1:] {
i := strings.Index(name, ":")
var keys []string
if i > 0 {
keys = strings.Split(name[i+1:], ",")
name = name[:i]
}
value, ok := os.LookupEnv(name)
if !ok {
fmt.Fprintf(os.Stderr, "%q variable not set", name)
os.Exit(1)
}
secrets := filepath.Join("/run/secrets", name)
if len(keys) == 0 {
// raw secret
fmt.Printf("inject secret %q info %s\n", name, secrets)
err := ioutil.WriteFile(secrets, []byte(value), 0444)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(3)
}
os.Exit(0)
}
var unmarshalled interface{}
err := json.Unmarshal([]byte(value), &unmarshalled)
if err == nil {
if dict, ok := unmarshalled.(map[string]interface{}); ok {
os.MkdirAll(secrets, 0555)
for k, v := range dict {
if !contains(keys, k) && !contains(keys, "*") {
continue
}
path := filepath.Join(secrets, k)
fmt.Printf("inject secret %q info %s\n", k, path)
var raw []byte
if s, ok := v.(string); ok {
raw = []byte(s)
} else {
raw, err = json.Marshal(v)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(2)
}
}
err = ioutil.WriteFile(path, raw, 0444)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(3)
}
}
os.Exit(0)
}
}
}
}
func contains(keys []string, s string) bool {
for _, k := range keys {
if k == s {
return true
}
}
return false
}