Skip to content

Commit

Permalink
Merge pull request #687 from APTy/tj/docker-attestor
Browse files Browse the repository at this point in the history
agent/workloadattestor: Add docker plugin
  • Loading branch information
azdagron authored Jan 24, 2019
2 parents 9ed03b4 + 718fed1 commit fcce613
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 2 deletions.
4 changes: 4 additions & 0 deletions conf/agent/agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ plugins {
plugin_data {
}
}
WorkloadAttestor "docker" {
plugin_data {
}
}
}
37 changes: 37 additions & 0 deletions doc/plugin_agent_workloadattestor_docker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent plugin: WorkloadAttestor "docker"

The `docker` plugin generates selectors based on docker labels for workloads calling the agent.
It does so by retrieving the workload's container ID from its cgroup membership, then querying
the docker daemon for the container's labels.

| Configuration | Description |
| ------------- | ----------- |
| docker_socket_path | The location of the docker daemon socket (default: "unix:///var/run/docker.sock" on unix). |
| docker_version | The API version of the docker daemon (default: "1.25").

Since selectors are created dynamically based on the container's docker labels, there isn't a list of known selectors.
Instead, each of the container's labels are used in creating the list of selectors.

| Selector | Example | Description |
| ----------------- | ----------------------------------- | ----------------------------------------------------- |
| `docker:label` | `docker:label:com.example.name:foo` | The key:value pair of each of the container's labels. |
| `docker:image_id` | `docker:image_id:77af4d6b9913` | The image id of the container. |

## Example
### Labels
If a workload container is started with `docker run --label com.example.name=foo [...]`, then workload registration would occur as:
```
spire-server entry create \
-parentID spiffe://example.org/host \
-spiffeID spiffe://example.org/host/foo \
-selector docker:label:com.example.name:foo
```

You can compose multiple labels as selectors.
```
spire-server entry create \
-parentID spiffe://example.org/host \
-spiffeID spiffe://example.org/host/foo \
-selector docker:label:com.example.name:foo
-selector docker:label:com.example.env:prod
```
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ module github.com/spiffe/spire
require (
cloud.google.com/go v0.34.0 // indirect
github.com/Azure/azure-sdk-for-go v19.1.0+incompatible
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Azure/go-autorest v10.15.2+incompatible
github.com/Microsoft/go-winio v0.4.11 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/armon/go-metrics v0.0.0-20180713145231-3c58d8115a78
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go v1.15.24
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dimchansky/utfbom v1.0.0 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190123164140-de86ba27fbea
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/envoyproxy/go-control-plane v0.6.6
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/go-ini/ini v1.38.2 // indirect
Expand All @@ -24,6 +31,8 @@ require (
github.com/golang/protobuf v1.2.0
github.com/google/go-cmp v0.2.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.4.1
github.com/hashicorp/go-hclog v0.0.0-20180828044259-75ecd6e6d645
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
Expand All @@ -42,7 +51,10 @@ require (
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/onsi/ginkgo v1.7.0 // indirect
github.com/onsi/gomega v1.4.3 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/posener/complete v1.1.2 // indirect
github.com/shirou/gopsutil v0.0.0-20180801053943-8048a2e9c577
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
Expand All @@ -65,4 +77,5 @@ require (
gopkg.in/ini.v1 v1.40.0 // indirect
gopkg.in/square/go-jose.v2 v2.1.8
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
gotest.tools v2.2.0+incompatible // indirect
)
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/azure-sdk-for-go v19.1.0+incompatible h1:ysqLW+tqZjJWOTE74heH/pDRbr4vlN3yV+dqQYgpyxw=
github.com/Azure/azure-sdk-for-go v19.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v10.15.2+incompatible h1:oZpnRzZie83xGV5txbT1aa/7zpCPvURGhV6ThJij2bs=
github.com/Azure/go-autorest v10.15.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/armon/go-metrics v0.0.0-20180713145231-3c58d8115a78 h1:mdRSArcFLfW0VoL34LZAKSz6LkkK4jFxVx2xYavACMg=
Expand All @@ -23,6 +29,14 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.0.0 h1:fGC2kkf4qOoKqZ4q7iIh+Vef4ubC1c38UDsEyZynZPc=
github.com/dimchansky/utfbom v1.0.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.7.3-0.20190123164140-de86ba27fbea h1:Bk0m1+4R5L8Z41SxoQsbLozrhnUTfLONQGyqPcqpY84=
github.com/docker/docker v0.7.3-0.20190123164140-de86ba27fbea/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.6.6 h1:vIu840n6c17xjQK9NDM3pfU5u1xiAzdbdwTog9V/MlU=
github.com/envoyproxy/go-control-plane v0.6.6/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
Expand Down Expand Up @@ -54,6 +68,10 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/grpc-ecosystem/grpc-gateway v1.4.1 h1:pX7cnDwSSmG0dR9yNjCQSSpmsJOqFdT7SzVp5Yl9uVw=
github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
Expand Down Expand Up @@ -107,8 +125,14 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
Expand Down Expand Up @@ -169,3 +193,5 @@ gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
6 changes: 4 additions & 2 deletions pkg/agent/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/spiffe/spire/pkg/agent/plugin/nodeattestor/jointoken"
k8s_na "github.com/spiffe/spire/pkg/agent/plugin/nodeattestor/k8s"
"github.com/spiffe/spire/pkg/agent/plugin/nodeattestor/x509pop"
"github.com/spiffe/spire/pkg/agent/plugin/workloadattestor/docker"
k8s_wa "github.com/spiffe/spire/pkg/agent/plugin/workloadattestor/k8s"
"github.com/spiffe/spire/pkg/agent/plugin/workloadattestor/unix"
"github.com/spiffe/spire/proto/agent/keymanager"
Expand Down Expand Up @@ -57,8 +58,9 @@ var (
"k8s_sat": nodeattestor.NewBuiltIn(k8s_na.NewSATAttestorPlugin()),
},
WorkloadAttestorType: {
"k8s": workloadattestor.NewBuiltIn(k8s_wa.New()),
"unix": workloadattestor.NewBuiltIn(unix.New()),
"k8s": workloadattestor.NewBuiltIn(k8s_wa.New()),
"unix": workloadattestor.NewBuiltIn(unix.New()),
"docker": workloadattestor.NewBuiltIn(docker.New()),
},
}
)
Expand Down
161 changes: 161 additions & 0 deletions pkg/agent/plugin/workloadattestor/docker/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package docker

import (
"context"
"fmt"
"log"
"strings"
"sync"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client"
"github.com/hashicorp/hcl"
"github.com/spiffe/spire/pkg/agent/common/cgroups"
"github.com/spiffe/spire/proto/agent/workloadattestor"
"github.com/spiffe/spire/proto/common"
spi "github.com/spiffe/spire/proto/common/plugin"
)

const (
selectorType = "docker"
subselectorLabel = "label"
subselectorImageID = "image_id"
defaultCgroupPrefix = "/docker"
)

var defaultContainerIndex = 1

// DockerClient is a subset of the docker client functionality, useful for mocking.
type DockerClient interface {
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
}

type dockerPlugin struct {
docker DockerClient
cgroupPrefix string
cgroupContainerIndex int
fs cgroups.FileSystem
mtx *sync.RWMutex
}

type dockerPluginConfig struct {
// DockerSocketPath is the location of the docker daemon socket (default: "unix:///var/run/docker.sock" on unix).
DockerSocketPath string `hcl:"docker_socket_path"`
// DockerVersion is the API version of the docker daemon (default: "1.40").
DockerVersion string `hcl:"docker_version"`
// CgroupPrefix is the cgroup prefix to look for in the cgroup entries (default: "/docker").
CgroupPrefix string `hcl:"cgroup_prefix"`
// CgroupContainerIndex is the index within the cgroup path where the container ID should be found (default: 1).
// This is a *int to allow differentiation between the default int value (0) and the absence of the field.
CgroupContainerIndex *int `hcl:"cgroup_container_index"`
}

func (p *dockerPlugin) Attest(ctx context.Context, req *workloadattestor.AttestRequest) (*workloadattestor.AttestResponse, error) {
p.mtx.RLock()
defer p.mtx.RUnlock()

cgroupList, err := cgroups.GetCgroups(req.Pid, p.fs)
if err != nil {
return nil, err
}

var containerID string
var hasDockerEntries bool
for _, cgroup := range cgroupList {
// We are only interested in cgroup entries that match our desired prefix. Example entry:
// "10:perf_event:/docker/2235ebefd9babe0dde4df4e7c49708e24fb31fb851edea55c0ee29a18273cdf4"
if !strings.HasPrefix(cgroup.GroupPath, p.cgroupPrefix) {
continue
}
hasDockerEntries = true

parts := strings.Split(cgroup.GroupPath, "/")

if len(parts) <= p.cgroupContainerIndex+1 {
log.Printf("Docker entry found, but is missing the container id: %v", cgroup.GroupPath)
continue
}
containerID = parts[p.cgroupContainerIndex+1]
break
}

// Not a docker workload. Since it is expected that non-docker workloads will call the
// workload API, it is fine to return a response without any selectors.
if !hasDockerEntries {
return &workloadattestor.AttestResponse{}, nil
}
if containerID == "" {
return nil, fmt.Errorf("workloadattestor/docker: no cgroup %q entries found at index %d", p.cgroupPrefix, p.cgroupContainerIndex)
}

container, err := p.docker.ContainerInspect(ctx, containerID)
if err != nil {
return nil, err
}

return &workloadattestor.AttestResponse{
Selectors: getSelectorsFromConfig(container.Config),
}, nil
}

func getSelectorsFromConfig(cfg *container.Config) []*common.Selector {
var selectors []*common.Selector
for label, value := range cfg.Labels {
selectors = append(selectors, &common.Selector{
Type: selectorType,
Value: fmt.Sprintf("%s:%s:%s", subselectorLabel, label, value),
})
}
if cfg.Image != "" {
selectors = append(selectors, &common.Selector{
Type: selectorType,
Value: fmt.Sprintf("%s:%s", subselectorImageID, cfg.Image),
})
}
return selectors
}

func (p *dockerPlugin) Configure(ctx context.Context, req *spi.ConfigureRequest) (*spi.ConfigureResponse, error) {
p.mtx.Lock()
defer p.mtx.Unlock()

var err error
config := &dockerPluginConfig{}
if err = hcl.Decode(config, req.Configuration); err != nil {
return nil, err
}

var opts []func(*dockerclient.Client) error
if config.DockerSocketPath != "" {
opts = append(opts, dockerclient.WithHost(config.DockerSocketPath))
}
if config.DockerVersion != "" {
opts = append(opts, dockerclient.WithVersion(config.DockerVersion))
}
p.docker, err = dockerclient.NewClientWithOpts(opts...)
if err != nil {
return nil, err
}
if config.CgroupPrefix == "" {
config.CgroupPrefix = defaultCgroupPrefix
}
if config.CgroupContainerIndex == nil {
config.CgroupContainerIndex = &defaultContainerIndex
}
p.cgroupPrefix = config.CgroupPrefix
p.cgroupContainerIndex = *config.CgroupContainerIndex

return &spi.ConfigureResponse{}, nil
}

func (*dockerPlugin) GetPluginInfo(context.Context, *spi.GetPluginInfoRequest) (*spi.GetPluginInfoResponse, error) {
return &spi.GetPluginInfoResponse{}, nil
}

func New() *dockerPlugin {
return &dockerPlugin{
mtx: &sync.RWMutex{},
fs: cgroups.OSFileSystem{},
}
}
Loading

0 comments on commit fcce613

Please sign in to comment.