-
Notifications
You must be signed in to change notification settings - Fork 486
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #687 from APTy/tj/docker-attestor
agent/workloadattestor: Add docker plugin
- Loading branch information
Showing
9 changed files
with
529 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,4 +26,8 @@ plugins { | |
plugin_data { | ||
} | ||
} | ||
WorkloadAttestor "docker" { | ||
plugin_data { | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{}, | ||
} | ||
} |
Oops, something went wrong.