Skip to content

Commit

Permalink
Merge pull request #557 from enricoschiattarella/nodeattestor
Browse files Browse the repository at this point in the history
K8s server and agent nodeattestor plugin
  • Loading branch information
azdagron committed Oct 19, 2018
2 parents 4a07ef4 + 73f4a67 commit b47a0ba
Show file tree
Hide file tree
Showing 12 changed files with 1,296 additions and 10 deletions.
46 changes: 46 additions & 0 deletions doc/plugin_agent_nodeattestor_k8s_sat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Agent plugin: NodeAttestor "k8s_sat"

*Must be used in conjunction with the server-side k8s_sat plugin*

The `k8s_sat` plugin attests nodes running in inside of Kubernetes. The agent
reads and provides the signed service account token to the server. It also
generates a one-time UUID that is also provided to the server.

The plugin generates SPIFFE IDs with the form:

```
spiffe://<trust domain>/spire/agent/k8s_sat/<cluster>/<UUID>
```

The main configuration accepts the following values:

| Configuration | Description | Default |
| --------------- | ----------- | ----------------------- |
| `cluster` | Name of the cluster. It must correspond to a cluster configured in the server plugin. |
| `token_path` | Path to the service account token on disk | "/run/secrets/kubernetes.io/serviceaccount/token" |

The token path defaults to the default location kubernetes uses to place the token and should not need to be overriden in most cases.

A sample configuration with the default token path:

```
NodeAttestor "k8s_sat" {
plugin_data {
cluster = "MyCluster"
}
}
```

## Security Considerations

At this time, the service account token does not contain claims that could be
used to strongly identify the node/daemonset/pod running the agent. This means
that any container running in a whitelisted service account can masquerade as
an agent, giving it access to any identity the agent is capable of issuing. It
is **STRONGLY** recommended that agents run under a dedicated service account.

It should be noted that due to the fact that SPIRE can't positively
identify a node using this method, it is not possible to directly authorize
identities for a distinct node or sets of nodes. Instead, this must be
accomplished indirectly using a service account and deployment that
leverages node affinity or node selectors.
65 changes: 65 additions & 0 deletions doc/plugin_server_nodeattestor_k8s_sat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Server plugin: NodeAttestor "k8s_sat"

*Must be used in conjunction with the agent-side k8s_sat plugin*

The `k8s_sat` plugin attests nodes running in inside of Kubernetes. The server
validates the signed service account token provided by the agent. It extracts
the service account name and namespace from the token claims. The server uses a
one-time UUID provided by the agent to generate a SPIFFE ID with the form:

```
spiffe://<trust domain>/spire/agent/k8s_sat/<cluster>/<UUID>
```

The server does not need to be running in Kubernetes in order to perform node
attestation. In fact, the plugin can be configured to attest nodes running in
multiple clusters.

The main configuration accepts the following values:

| Configuration | Description | Default |
| --------------- | ----------- | ----------------------- |
| `clusters` | A map of clusters, keyed by an arbitrary ID, that are authorized for attestation. | |

Each cluster in the main configuration requires the following configuration:

| Configuration | Description | Default |
| ------------- | ----------- | ----------------------- |
| `service_account_key_file` | Path on disk to a PEM encoded file containing public keys used in validating tokens for that cluster. RSA and ECDSA keys are supported. For RSA, X509 certificates, PKCS1, and PKIX encoded public keys are accepted. For ECDSA, X509 certificates, and PKIX encoded public keys are accepted. | |
| `service_account_whitelist` | A list of service account names, qualified by namespace (for example, "default:blog" or "production:web") to allow for node attestation. Attestation will be rejected for tokens bound to service accounts that aren't in the whitelist. | |

A sample configuration:

```
NodeAttestor "k8s_sat" {
plugin_data {
clusters = {
"MyCluster" = {
service_account_key_file = "/path/to/key/file"
service_account_whitelist = ["production:spire-agent"]
}
}
}
```

In addition, this plugin generates the following selectors:

| Selector | Example | Description |
| -------------------| ------------------------------ | ------------------------------------------ |
| `k8s_sat:cluster` | `k8s_sat:cluster:MyCluster` | Name of the cluster (from the plugin config) used to verify the token signature |
| `k8s_sat:agent_ns` | `k8s_sat:agent_ns:production` | Namespace that the agent is running under |
| `k8s_sat:agent_sa` | `k8s_sat:agent_sa:spire-agent` | Service Account the agent is running under |

## Security Considerations

At this time, the service account token does not contain claims that could be
used to strongly identify the node/daemonset/pod running the agent. This means
that any container running in a whitelisted service account can masquerade as
an agent, giving it access to any identity the agent is capable of issuing. It
is **STRONGLY** recommended that agents run under a dedicated service account.

It should be noted that due to the fact that SPIRE can't positively
identify a node using this method, it is not possible to directly authorize
identities for a distinct node or sets of nodes. Instead, this must be
accomplished indirectly using a service account and deployment that
leverages node affinity or node selectors.
1 change: 1 addition & 0 deletions doc/spire_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ communicates with spire-server via the Node API.
| NodeAttestor | [aws_iid](/doc/plugin_agent_nodeattestor_aws_iid.md) | An AWS IID attestor that automatically attests EC2 instances using the AWS Instance Metadata API and the AWS Instance Identity document. |
| NodeAttestor | [azure_msi](/doc/plugin_agent_nodeattestor_azure_msi.md) | An Azure Node attestor that automatically attests Azure VMs using a signed Managed Service Identity (MSI) token. |
| NodeAttestor | [gcp_iit](/doc/plugin_agent_nodeattestor_gcp_iit.md) | An Google Compute Engine Node attestor that automatically attests GCE instances using a signed token from Google retrieved via the Compute Engine Metadata API. |
| NodeAttestor | [k8s_sat](/doc/plugin_agent_nodeattestor_k8s_sat.md) | A node attestor which attests agents using service account tokens inside of Kubernetes |
| WorkloadAttestor | [unix](/doc/plugin_agent_workloadattestor_unix.md) | A workload attestor which generates unix-based selectors like `uid` and `gid` |
| WorkloadAttestor | [k8s](/doc/plugin_agent_workloadattestor_k8s.md) | A workload attestor which allows selectors based on Kubernetes constructs such `ns` (namespace) and `sa` (service account)|

Expand Down
1 change: 1 addition & 0 deletions doc/spire_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ API and the Node API, with which agents communicate with the server.
| NodeAttestor | [nodeattestor_azure_msi](/doc/plugin_server_nodeattestor_azure_msi.md) | A node attestor which validates agents attesting using the [azure_msi](/doc/plugin_agent_nodeattestor_azure_msi.md) node attestor plugin. |
| NodeResolver | [noderesolver_azure_msi](/doc/plugin_server_noderesolver_aws_iid.md) | A node resolver which extends the [nodeattestor_azure_msi](/doc/plugin_server_nodeattestor_azure_msi.md) node attestor plugin to support selecting nodes based on additional properties (such as Network Security Group). |
| NodeAttestor | [nodeattestor_gcp_iit](/doc/plugin_server_nodeattestor_gcp_iit.md) | A node attestor which validates agents attesting using the [gcp_iit](/doc/plugin_agent_nodeattestor_gcp_iit.md) node attestor plugin. |
| NodeAttestor | [nodeattestor_k8s_sat](/doc/plugin_server_nodeattestor_k8s.md) | A node attestor which validates agents using the [k8s_sat](/doc/plugin_agent_nodeattestor_k8s_sat.md) node attestor plugin. |
| NodeResolver | [noop](/doc/plugin_server_noderesolver_noop.md) | It is mandatory to have at least one node resolver plugin configured. This one is a no-op |
| UpstreamCA | [disk](/doc/plugin_server_upstreamca_disk.md) | Uses a CA loaded from disk to generate SPIRE server intermediate certificates for use in the ServerCA plugin |

Expand Down
6 changes: 4 additions & 2 deletions pkg/agent/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import (
"github.com/spiffe/spire/pkg/agent/plugin/nodeattestor/azure"
"github.com/spiffe/spire/pkg/agent/plugin/nodeattestor/gcp"
"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/k8s"
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"
"github.com/spiffe/spire/proto/agent/nodeattestor"
Expand Down Expand Up @@ -53,9 +54,10 @@ var (
"gcp_iit": nodeattestor.NewBuiltIn(gcp.NewIITAttestorPlugin()),
"x509pop": nodeattestor.NewBuiltIn(x509pop.New()),
"azure_msi": nodeattestor.NewBuiltIn(azure.NewMSIAttestorPlugin()),
"k8s_sat": nodeattestor.NewBuiltIn(k8s_na.NewSATAttestorPlugin()),
},
WorkloadAttestorType: {
"k8s": workloadattestor.NewBuiltIn(k8s.New()),
"k8s": workloadattestor.NewBuiltIn(k8s_wa.New()),
"unix": workloadattestor.NewBuiltIn(unix.New()),
},
}
Expand Down
146 changes: 146 additions & 0 deletions pkg/agent/plugin/nodeattestor/k8s/sat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package k8s

import (
"context"
"encoding/json"
"io/ioutil"
"sync"

"github.com/hashicorp/hcl"
uuid "github.com/satori/go.uuid"
"github.com/spiffe/spire/pkg/common/plugin/k8s"
"github.com/spiffe/spire/proto/agent/nodeattestor"
"github.com/spiffe/spire/proto/common"
spi "github.com/spiffe/spire/proto/common/plugin"
"github.com/zeebo/errs"
)

const (
pluginName = "k8s_sat"

defaultTokenPath = "/run/secrets/kubernetes.io/serviceaccount/token"
)

var (
satError = errs.Class("k8s-sat")
)

type SATAttestorConfig struct {
Cluster string `hcl:"cluster"`
TokenPath string `hcl:"token_path"`
}

type satAttestorConfig struct {
trustDomain string
cluster string
tokenPath string
}

type SATAttestorPlugin struct {
mu sync.RWMutex
config *satAttestorConfig

hooks struct {
newUUID func() string
}
}

var _ nodeattestor.Plugin = (*SATAttestorPlugin)(nil)

func NewSATAttestorPlugin() *SATAttestorPlugin {
p := &SATAttestorPlugin{}
p.hooks.newUUID = func() string {
return uuid.NewV4().String()
}
return p
}

func (p *SATAttestorPlugin) FetchAttestationData(stream nodeattestor.FetchAttestationData_PluginStream) error {
config, err := p.getConfig()
if err != nil {
return err
}

uuid := p.hooks.newUUID()

token, err := loadTokenFromFile(config.tokenPath)
if err != nil {
return satError.New("unable to load token from %s: %v", config.tokenPath, err)
}

data, err := json.Marshal(k8s.SATAttestationData{
Cluster: config.cluster,
UUID: uuid,
Token: token,
})
if err != nil {
return satError.Wrap(err)
}

return stream.Send(&nodeattestor.FetchAttestationDataResponse{
AttestationData: &common.AttestationData{
Type: pluginName,
Data: data,
},
SpiffeId: k8s.AgentID(config.trustDomain, config.cluster, uuid),
})
}

func (p *SATAttestorPlugin) Configure(ctx context.Context, req *spi.ConfigureRequest) (resp *spi.ConfigureResponse, err error) {
hclConfig := new(SATAttestorConfig)
if err := hcl.Decode(hclConfig, req.Configuration); err != nil {
return nil, satError.New("unable to decode configuration: %v", err)
}

if req.GlobalConfig == nil {
return nil, satError.New("global configuration is required")
}
if req.GlobalConfig.TrustDomain == "" {
return nil, satError.New("global configuration missing trust domain")
}
if hclConfig.Cluster == "" {
return nil, satError.New("configuration missing cluster")
}

config := &satAttestorConfig{
trustDomain: req.GlobalConfig.TrustDomain,
cluster: hclConfig.Cluster,
tokenPath: hclConfig.TokenPath,
}
if config.tokenPath == "" {
config.tokenPath = defaultTokenPath
}

p.setConfig(config)
return &spi.ConfigureResponse{}, nil
}

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

func (p *SATAttestorPlugin) getConfig() (*satAttestorConfig, error) {
p.mu.RLock()
defer p.mu.RUnlock()
if p.config == nil {
return nil, satError.New("not configured")
}
return p.config, nil
}

func (p *SATAttestorPlugin) setConfig(config *satAttestorConfig) {
p.mu.Lock()
defer p.mu.Unlock()
p.config = config
}

func loadTokenFromFile(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", errs.Wrap(err)
}
if len(data) == 0 {
return "", errs.New("%q is empty", path)
}
return string(data), nil
}
Loading

0 comments on commit b47a0ba

Please sign in to comment.