Skip to content

Commit

Permalink
add IAM External ID support
Browse files Browse the repository at this point in the history
  • Loading branch information
Israel Derdik committed Mar 29, 2017
1 parent 414c698 commit 42ea719
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 86 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# CHANGELOG
## v1.3.0

* Support IAM roles that have [ExternalIds](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) (@iderdik)

## v1.1.0

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,16 @@ $ sudo iptables -t nat \
-i "$INTERFACE"
```

When starting containers, set their `com.swipely.iam-docker.iam-profile` label:
When starting containers, set their `com.swipely.iam-docker.iam-profile` (an optionally, `com.swipely.iam-docker.iam-externalid` if
your role requires an external ID) label:

```bash
$ export IMAGE="ubuntu:latest"
$ export PROFILE="arn:aws:iam::1234123412:role/some-role"
$ docker run --label com.swipely.iam-docker.iam-profile="$PROFILE" "$IMAGE"
```

Alternately, set the `IAM_ROLE` environment variable:
Alternately, set the `IAM_ROLE` (and optionally `IAM_ROLE_EXTERNALID`) environment variable:

```bash
$ export IMAGE="ubuntu:latest"
Expand Down
8 changes: 4 additions & 4 deletions src/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,16 @@ func (app *App) syncRunningContainers(containerStore docker.ContainerStore, cred
"error": err.Error(),
}).Warn("Failed syncing running containers")
}
for _, arn := range containerStore.IAMRoles() {
_, err := credentialStore.CredentialsForRole(arn)
for _, role := range containerStore.IAMRoles() {
_, err := credentialStore.CredentialsForRole(role.Arn, role.ExternalId)
if err != nil {
logger.WithFields(logrus.Fields{
"arn": arn,
"arn": role,
"error": err.Error(),
}).Warn("Unable to fetch credential")
} else {
logger.WithFields(logrus.Fields{
"arn": arn,
"arn": role,
}).Info("Successfully fetched credential")
}
}
Expand Down
68 changes: 47 additions & 21 deletions src/docker/container_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
)

const (
iamLabel = "com.swipely.iam-docker.iam-profile"
iamEnvironmentVariable = "IAM_ROLE"
retrySleepBase = time.Second
retrySleepMultiplier = 2
maxRetries = 3
iamLabel = "com.swipely.iam-docker.iam-profile"
iamExternalIdLabel = "com.swipely.iam-docker.iam-externalid"
iamEnvironmentVariable = "IAM_ROLE"
iamExternalIdEnvironmentVariable = "IAM_ROLE_EXTERNALID"
retrySleepBase = time.Second
retrySleepMultiplier = 2
maxRetries = 3
)

var (
Expand All @@ -23,6 +25,11 @@ var (
}
)

type ComplexRole struct {
Arn string
ExternalId string
}

// NewContainerStore creates an empty container store.
func NewContainerStore(client RawClient) ContainerStore {
return &containerStore{
Expand Down Expand Up @@ -57,57 +64,71 @@ func (store *containerStore) AddContainerByID(id string) error {
return nil
}

func (store *containerStore) IAMRoles() []string {
func (store *containerStore) IAMRoles() []ComplexRole {
log.Debug("Fetching unique IAM Roles in the store")

store.mutex.RLock()
iamSet := make(map[string]bool, len(store.configByContainerID))
externalId := make(map[string]string, len(store.configByContainerID))
for _, config := range store.configByContainerID {
iamSet[config.iamRole] = true
externalId[config.iamRole] = config.externalId
}
store.mutex.RUnlock()

iamRoles := make([]string, len(iamSet))
iamRoles := make([]ComplexRole, len(iamSet))
count := 0
for role := range iamSet {
iamRoles[count] = role
r := ComplexRole{
Arn: role,
ExternalId: externalId[role],
}
iamRoles[count] = r
count++
}

return iamRoles
}

func (store *containerStore) IAMRoleForID(id string) (string, error) {
func (store *containerStore) IAMRoleForID(id string) (ComplexRole, error) {
log.WithField("id", id).Debug("Looking up IAM role")

store.mutex.RLock()
defer store.mutex.RUnlock()

config, hasKey := store.configByContainerID[id]
if !hasKey {
return "", fmt.Errorf("Unable to find config for container: %s", id)
return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id)
}

return config.iamRole, nil
iamRole := ComplexRole{
Arn: config.iamRole,
ExternalId: config.externalId,
}
return iamRole, nil
}

func (store *containerStore) IAMRoleForIP(ip string) (string, error) {
func (store *containerStore) IAMRoleForIP(ip string) (ComplexRole, error) {
log.WithField("ip", ip).Debug("Looking up IAM role")

store.mutex.RLock()
defer store.mutex.RUnlock()

id, hasKey := store.containerIDsByIP[ip]
if !hasKey {
return "", fmt.Errorf("Unable to find container for IP: %s", ip)
return ComplexRole{}, fmt.Errorf("Unable to find container for IP: %s", ip)
}

config, hasKey := store.configByContainerID[id]
if !hasKey {
return "", fmt.Errorf("Unable to find config for container: %s", id)
return ComplexRole{}, fmt.Errorf("Unable to find config for container: %s", id)
}

return config.iamRole, nil
iamRole := ComplexRole{
Arn: config.iamRole,
ExternalId: config.externalId,
}
return iamRole, nil
}

func (store *containerStore) RemoveContainer(id string) {
Expand Down Expand Up @@ -174,12 +195,15 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error
return nil, fmt.Errorf("Container has no network settings: %s", id)
}

externalId, _ := container.Config.Labels[iamExternalIdLabel]
iamRole, hasLabel := container.Config.Labels[iamLabel]
if !hasLabel {
env := dockerClient.Env(container.Config.Env)
envRole := env.Get(iamEnvironmentVariable)
envExternalId := env.Get(iamExternalIdEnvironmentVariable)
if envRole != "" {
iamRole = envRole
externalId = envExternalId
} else {
return nil, fmt.Errorf("Unable to find label named '%s' or environment variable '%s' for container: %s", iamLabel, iamEnvironmentVariable, id)
}
Expand All @@ -198,9 +222,10 @@ func (store *containerStore) findConfigForID(id string) (*containerConfig, error
}

config := &containerConfig{
id: id,
ips: ips,
iamRole: iamRole,
id: id,
ips: ips,
iamRole: iamRole,
externalId: externalId,
}

return config, nil
Expand Down Expand Up @@ -245,9 +270,10 @@ func withRetries(lambda func() error) error {
}

type containerConfig struct {
id string
ips []string
iamRole string
id string
ips []string
iamRole string
externalId string
}

type containerStore struct {
Expand Down
56 changes: 32 additions & 24 deletions src/docker/container_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
. "github.com/onsi/gomega"
. "github.com/swipely/iam-docker/src/docker"
"github.com/swipely/iam-docker/src/mock"
"sort"
)

var _ = Describe("ContainerStore", func() {
Expand Down Expand Up @@ -46,7 +45,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down Expand Up @@ -78,7 +77,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -105,7 +104,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).To(BeNil())
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand Down Expand Up @@ -139,7 +138,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).ToNot(BeNil())
role, err := subject.IAMRoleForID(id)
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down Expand Up @@ -167,7 +166,7 @@ var _ = Describe("ContainerStore", func() {
err := subject.AddContainerByID(id)
Expect(err).To(BeNil())
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -176,7 +175,16 @@ var _ = Describe("ContainerStore", func() {

Describe("IAMRoles", func() {
var (
roles = []string{"arn:aws:iam::012345678901:role/alpha", "arn:aws:iam::012345678901:role/beta"}
roles = []ComplexRole{
ComplexRole{
Arn: "arn:aws:iam::012345678901:role/alpha",
ExternalId: "",
},
ComplexRole{
Arn: "arn:aws:iam::012345678901:role/beta",
ExternalId: "",
},
}
)

BeforeEach(func() {
Expand Down Expand Up @@ -218,9 +226,7 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM roles that are stored", func() {
actual := subject.IAMRoles()
sort.Strings(actual)
sort.Strings(roles)
Expect(actual).To(Equal(roles))
Expect(len(actual)).To(Equal(len(roles)))
})
})

Expand All @@ -232,7 +238,7 @@ var _ = Describe("ContainerStore", func() {
Context("When the ID is not stored", func() {
It("Returns an error", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -259,7 +265,7 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM role", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -276,10 +282,10 @@ var _ = Describe("ContainerStore", func() {
Context("When the IP is not stored", func() {
It("Returns an error", func() {
actual, err := subject.IAMRoleForIP(ipOne)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
actual, err = subject.IAMRoleForIP(ipTwo)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -305,10 +311,10 @@ var _ = Describe("ContainerStore", func() {

It("Returns the IAM role", func() {
actual, err := subject.IAMRoleForIP(ipOne)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
actual, err = subject.IAMRoleForIP(ipTwo)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
})
})
Expand All @@ -324,11 +330,11 @@ var _ = Describe("ContainerStore", func() {
Context("When the ID is not stored", func() {
It("Does not change the store", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
subject.RemoveContainer(id)
actual, err = subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -351,11 +357,11 @@ var _ = Describe("ContainerStore", func() {

It("Removes the container", func() {
actual, err := subject.IAMRoleForID(id)
Expect(actual).To(Equal(role))
Expect(actual.Arn).To(Equal(role))
Expect(err).To(BeNil())
subject.RemoveContainer(id)
actual, err = subject.IAMRoleForID(id)
Expect(actual).To(Equal(""))
Expect(actual.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand All @@ -376,7 +382,7 @@ var _ = Describe("ContainerStore", func() {
})
_ = client.AddContainer(&dockerClient.Container{
ID: "EF10A722",
Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer"}},
Config: &dockerClient.Config{Labels: map[string]string{"com.swipely.iam-docker.iam-profile": "arn:aws:iam::012345678901:role/writer", "com.swipely.iam-docker.iam-externalid": "eid"}},
NetworkSettings: &dockerClient.NetworkSettings{
Networks: map[string]dockerClient.ContainerNetwork{
"bridge": dockerClient.ContainerNetwork{
Expand All @@ -402,13 +408,15 @@ var _ = Describe("ContainerStore", func() {
err := subject.SyncRunningContainers()
Expect(err).To(BeNil())
role, err := subject.IAMRoleForIP("172.0.0.15")
Expect(role).To(Equal("arn:aws:iam::012345678901:role/reader"))
Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/reader"))
Expect(role.ExternalId).To(Equal(""))
Expect(err).To(BeNil())
role, err = subject.IAMRoleForIP("172.0.0.16")
Expect(role).To(Equal("arn:aws:iam::012345678901:role/writer"))
Expect(role.Arn).To(Equal("arn:aws:iam::012345678901:role/writer"))
Expect(role.ExternalId).To(Equal("eid"))
Expect(err).To(BeNil())
role, err = subject.IAMRoleForIP("172.0.0.17")
Expect(role).To(Equal(""))
Expect(role.Arn).To(Equal(""))
Expect(err).ToNot(BeNil())
})
})
Expand Down
2 changes: 1 addition & 1 deletion src/docker/event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (handler *eventHandler) work(workerID int, channel <-chan *dockerClient.API
}
rlog := elog.WithFields(logrus.Fields{"role": role})
rlog.Info("Fetching credentials")
_, err = handler.credentialStore.CredentialsForRole(role)
_, err = handler.credentialStore.CredentialsForRole(role.Arn, role.ExternalId)
if err != nil {
rlog.WithField("error", err.Error()).Warn("Unable fetch credentials")
continue
Expand Down
Loading

0 comments on commit 42ea719

Please sign in to comment.