-
Notifications
You must be signed in to change notification settings - Fork 485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add http node attestor #4909
Merged
Merged
Add http node attestor #4909
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
b322773
Add http challenge node attestor
kfox1111 f08d9d2
Fix various issues so it works again after refactor
kfox1111 009166f
Fix some issues
kfox1111 7caac6f
Fix some issues
kfox1111 3e68d07
Fix some issues
kfox1111 baaa4f6
Fix some issues
kfox1111 029f117
Fix some issues
kfox1111 82af10b
Fix some issues
kfox1111 86fbf7c
Fix some issues
kfox1111 727c5f9
Implement tofu. Incorperate feedback
kfox1111 9f42248
Fix some lint bits
kfox1111 76d761f
More lint
kfox1111 a693947
More lint
kfox1111 28dc797
Merge branch 'main' into http
kfox1111 6416f01
Merge branch 'main' into http
kfox1111 9e8897e
Apply suggestions from code review
kfox1111 fdfc837
Incorperate feedback
kfox1111 f8bc768
Merge branch 'http' of https://github.com/kfox1111/spire into http
kfox1111 b5a439e
Incorperate feedback
kfox1111 0656513
Incorperate feedback
kfox1111 6a94dfd
Fix example
kfox1111 4e8133e
Incorperate feedback
kfox1111 b0df1d6
Incorperate feedback
kfox1111 0c1d254
Incorperate feedback
kfox1111 effb6b3
Fix lint issues
kfox1111 d6f3b6a
Fix lint issues
kfox1111 6cdb65b
Fix 404 issue
kfox1111 98d7363
Fix lint issue
kfox1111 623ed12
Fix issue with protocol
kfox1111 968e90e
Merge branch 'main' into http
kfox1111 0e7dd47
Remove tempate
kfox1111 b687049
Apply suggestions from code review
kfox1111 1caaaec
Merge branch 'main' into http
kfox1111 069bb2b
Fix typo
kfox1111 69edbe1
Add some unit tests
kfox1111 41f1e74
Add a bunch of httpchallenge server plugin tests
kfox1111 92e72d5
Add some more tests
kfox1111 151d3c3
Test server works
kfox1111 b6c4a41
Add tofu checks
kfox1111 f00d02f
Add httpchallenge agent tests
kfox1111 81b205a
Incorperate feedback
kfox1111 501a957
gofmt files
kfox1111 a404aa3
Apply suggestions from code review
kfox1111 5ab497d
Fix lint issues
kfox1111 b5caee3
Fix lint issues
kfox1111 1af2187
Fix test
kfox1111 a2c0385
Incorperate feedback. Add localhost block.
kfox1111 be2b414
Fix test
kfox1111 2bec5df
Add test for localhost test. Incorperate feedback
kfox1111 4c95d25
Incorperate feedback
kfox1111 2ac078f
Fix lint issue
kfox1111 32f8f04
Merge branch 'main' into http
kfox1111 f47af5c
Merge branch 'main' into http
kfox1111 f596796
Update doc/plugin_server_nodeattestor_http_challenge.md
kfox1111 2880654
Merge branch 'main' into http
kfox1111 3dae7a2
Incorperate feedback
kfox1111 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Agent plugin: NodeAttestor "http_challenge" | ||
|
||
*Must be used in conjunction with the server-side http_challenge plugin* | ||
|
||
The `http_challenge` plugin handshakes via http to ensure the agent is running on a valid | ||
dns name. | ||
|
||
The SPIFFE ID produced by the server-side `http_challenge` plugin is based on the dns name of the agent. | ||
The SPIFFE ID has the form: | ||
|
||
```xml | ||
spiffe://<trust_domain>/spire/agent/http_challenge/<hostname> | ||
``` | ||
|
||
| Configuration | Description | Default | | ||
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-----------| | ||
| `hostname` | Hostname to use for handshaking. If unset, it will be automatically detected. | | | ||
| `agentname` | Name of this agent on the host. Useful if you have multiple agents bound to different spire servers on the same host and sharing the same port. | "default" | | ||
| `port` | The port to listen on. If unspecified, a random value will be used. | random | | ||
| `advertised_port` | The port to tell the server to call back on. | $port | | ||
|
||
If `advertised_port` != `port`, you will need to setup an http proxy between the two ports. This is useful if you already run a webserver on port 80. | ||
|
||
A sample configuration: | ||
|
||
```hcl | ||
NodeAttestor "http_challenge" { | ||
plugin_data { | ||
port = 80 | ||
} | ||
} | ||
``` | ||
|
||
## Proxies | ||
|
||
kfox1111 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Say you want to validate using port 80 to be internet firewall friendly. If you already have a webserver on port 80 or want to use multiple agents with different SPIRE servers and use the same port, | ||
you can have your webserver proxy over to the SPIRE agent(s) by setting up a proxy on `/.well-known/spiffe/nodeattestor/http_challenge/$agentname` to | ||
`http://localhost:$port/.well-known/spiffe/nodeattestor/http_challenge/$agentname`. | ||
|
||
Example spire agent configuration: | ||
|
||
```hcl | ||
NodeAttestor "http_challenge" { | ||
plugin_data { | ||
port = 8080 | ||
advertised_port = 80 | ||
} | ||
} | ||
``` |
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,55 @@ | ||
# Server plugin: NodeAttestor "http_challenge" | ||
|
||
*Must be used in conjunction with the agent-side http_challenge plugin* | ||
|
||
The `http_challenge` plugin handshakes via http to ensure the agent is running on a valid | ||
dns name. | ||
|
||
The SPIFFE ID produced by the plugin is based on the dns name attested. | ||
The SPIFFE ID has the form: | ||
|
||
```xml | ||
spiffe://<trust_domain>/spire/agent/http_challenge/<hostname> | ||
``` | ||
|
||
| Configuration | Description | Default | | ||
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------| | ||
| `allowed_dns_patterns` | A list of regular expressions to match to the hostname being attested. If none match, attestation will fail. If unset, all hostnames are allowed. | | | ||
| `required_port` | Set to a port number to require clients to listen only on that port. If unset, all port numbers are allowed | | | ||
| `allow_non_root_ports` | Set to true to allow ports >= 1024 to be used by the agents with the advertised_port | true | | ||
| `tofu` | Trust on first use of the successful challenge. Can only be disabled if allow_non_root_ports=false or required_port < 1024 | true | | ||
|
||
A sample configuration: | ||
|
||
```hcl | ||
NodeAttestor "http_challenge" { | ||
plugin_data { | ||
# Only match hosts that start with p, have a number, then end in example.com. Ex: 'p1.example.com' | ||
allowed_dns_patterns = ["p[0-9]\.example\.com"] | ||
|
||
# Only allow clients to use port 80 | ||
required_port = 80 | ||
|
||
# Change the agent's SPIFFE ID format | ||
# agent_path_template = "/spire/agent/http_challenge/{{ .Hostname }}" | ||
} | ||
} | ||
``` | ||
|
||
## Selectors | ||
|
||
| Selector | Example | Description | | ||
|----------|------------------------------------------|------------------------| | ||
| Hostname | `http_challenge:hostname:p1.example.com` | The Subject's Hostname | | ||
|
||
## Security Considerations | ||
|
||
Generally, TCP ports are accessible to any user of the node. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run. | ||
|
||
The `http_challenge` node attestor implements multiple features to mitigate the risk. | ||
|
||
Trust On First Use (or TOFU) is one such option. For any given node, attestation may occur only once when enabled. Subsequent attestation attempts will be rejected. | ||
|
||
With TOFU, it is still possible for non-agent code to complete node attestation before SPIRE Agent can, however this condition is easily and quickly detectable as SPIRE Agent will fail to start, and both SPIRE Agent and SPIRE Server will log the occurrence. Such cases should be investigated as possible security incidents. | ||
|
||
You also can require the port to be a trusted port that only trusted user such as root can open (port number < 1024). |
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
234 changes: 234 additions & 0 deletions
234
pkg/agent/plugin/nodeattestor/httpchallenge/httpchallenge.go
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,234 @@ | ||
package httpchallenge | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"os" | ||
"sync" | ||
"time" | ||
|
||
"github.com/hashicorp/go-hclog" | ||
"github.com/hashicorp/hcl" | ||
nodeattestorv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/plugin/agent/nodeattestor/v1" | ||
configv1 "github.com/spiffe/spire-plugin-sdk/proto/spire/service/common/config/v1" | ||
"github.com/spiffe/spire/pkg/common/catalog" | ||
"github.com/spiffe/spire/pkg/common/plugin/httpchallenge" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
const ( | ||
pluginName = "http_challenge" | ||
) | ||
|
||
func BuiltIn() catalog.BuiltIn { | ||
return builtin(New()) | ||
} | ||
|
||
func BuiltInWithHostname(hostname string) catalog.BuiltIn { | ||
plugin := New() | ||
plugin.hostname = hostname | ||
return builtin(plugin) | ||
} | ||
|
||
func builtin(p *Plugin) catalog.BuiltIn { | ||
return catalog.MakeBuiltIn(pluginName, | ||
nodeattestorv1.NodeAttestorPluginServer(p), | ||
configv1.ConfigServiceServer(p)) | ||
} | ||
|
||
type configData struct { | ||
port int | ||
advertisedPort int | ||
hostName string | ||
agentName string | ||
} | ||
|
||
type Config struct { | ||
HostName string `hcl:"hostname"` | ||
AgentName string `hcl:"agentname"` | ||
Port int `hcl:"port"` | ||
AdvertisedPort int `hcl:"advertised_port"` | ||
} | ||
|
||
type Plugin struct { | ||
nodeattestorv1.UnsafeNodeAttestorServer | ||
configv1.UnsafeConfigServer | ||
|
||
m sync.Mutex | ||
c *Config | ||
|
||
log hclog.Logger | ||
|
||
hostname string | ||
} | ||
|
||
func New() *Plugin { | ||
return &Plugin{} | ||
} | ||
|
||
func (p *Plugin) AidAttestation(stream nodeattestorv1.NodeAttestor_AidAttestationServer) (err error) { | ||
data, err := p.loadConfigData() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
ctx := stream.Context() | ||
|
||
port := data.port | ||
|
||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) | ||
if err != nil { | ||
return status.Errorf(codes.Internal, "could not listen on port %d: %v", port, err) | ||
} | ||
defer l.Close() | ||
|
||
advertisedPort := data.advertisedPort | ||
if advertisedPort == 0 { | ||
advertisedPort = l.Addr().(*net.TCPAddr).Port | ||
} | ||
|
||
attestationPayload, err := json.Marshal(httpchallenge.AttestationData{ | ||
HostName: data.hostName, | ||
AgentName: data.agentName, | ||
Port: advertisedPort, | ||
}) | ||
if err != nil { | ||
return status.Errorf(codes.Internal, "unable to marshal attestation data: %v", err) | ||
} | ||
|
||
// send the attestation data back to the agent | ||
if err := stream.Send(&nodeattestorv1.PayloadOrChallengeResponse{ | ||
Data: &nodeattestorv1.PayloadOrChallengeResponse_Payload{ | ||
Payload: attestationPayload, | ||
}, | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
// receive challenge | ||
resp, err := stream.Recv() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
challenge := new(httpchallenge.Challenge) | ||
if err := json.Unmarshal(resp.Challenge, challenge); err != nil { | ||
return status.Errorf(codes.Internal, "unable to unmarshal challenge: %v", err) | ||
} | ||
|
||
// due to https://github.com/spiffe/spire/blob/8f9fa036e182a2fab968e03cd25a7fdb2d8c88bb/pkg/agent/plugin/nodeattestor/v1.go#L63, we must respond with a non blank challenge response | ||
responseBytes := []byte{'\n'} | ||
if err := stream.Send(&nodeattestorv1.PayloadOrChallengeResponse{ | ||
Data: &nodeattestorv1.PayloadOrChallengeResponse_ChallengeResponse{ | ||
ChallengeResponse: responseBytes, | ||
}, | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
err = p.serveNonce(ctx, l, data.agentName, challenge.Nonce) | ||
if err != nil { | ||
return status.Errorf(codes.Internal, "failed to start webserver: %v", err) | ||
} | ||
return nil | ||
} | ||
|
||
func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (*configv1.ConfigureResponse, error) { | ||
// Parse HCL config payload into config struct | ||
config := new(Config) | ||
if err := hcl.Decode(config, req.HclConfiguration); err != nil { | ||
return nil, status.Errorf(codes.InvalidArgument, "unable to decode configuration: %v", err) | ||
} | ||
|
||
// Make sure the configuration produces valid data | ||
if _, err := loadConfigData(p.hostname, config); err != nil { | ||
return nil, err | ||
} | ||
|
||
p.setConfig(config) | ||
|
||
return &configv1.ConfigureResponse{}, nil | ||
} | ||
|
||
func (p *Plugin) serveNonce(ctx context.Context, l net.Listener, agentName string, nonce string) (err error) { | ||
h := http.NewServeMux() | ||
s := &http.Server{ | ||
Handler: h, | ||
ReadTimeout: 10 * time.Second, | ||
WriteTimeout: 10 * time.Second, | ||
} | ||
path := fmt.Sprintf("/.well-known/spiffe/nodeattestor/http_challenge/%s/challenge", agentName) | ||
p.log.Debug("Setting up nonce handler", "path", path) | ||
h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprintln(w, nonce) | ||
}) | ||
|
||
go func() { | ||
<-ctx.Done() | ||
_ = s.Shutdown(context.Background()) | ||
}() | ||
|
||
err = s.Serve(l) | ||
if err == http.ErrServerClosed { | ||
return nil | ||
} | ||
return err | ||
} | ||
|
||
// SetLogger sets this plugin's logger | ||
func (p *Plugin) SetLogger(log hclog.Logger) { | ||
p.log = log | ||
} | ||
|
||
func (p *Plugin) getConfig() *Config { | ||
p.m.Lock() | ||
defer p.m.Unlock() | ||
return p.c | ||
} | ||
|
||
func (p *Plugin) setConfig(c *Config) { | ||
p.m.Lock() | ||
defer p.m.Unlock() | ||
p.c = c | ||
} | ||
|
||
func (p *Plugin) loadConfigData() (*configData, error) { | ||
config := p.getConfig() | ||
if config == nil { | ||
return nil, status.Error(codes.FailedPrecondition, "not configured") | ||
} | ||
return loadConfigData(p.hostname, config) | ||
} | ||
|
||
func loadConfigData(hostname string, config *Config) (*configData, error) { | ||
if config.HostName == "" { | ||
if hostname != "" { | ||
config.HostName = hostname | ||
} else { | ||
var err error | ||
config.HostName, err = os.Hostname() | ||
if err != nil { | ||
return nil, status.Errorf(codes.InvalidArgument, "unable to fetch hostname: %v", err) | ||
} | ||
} | ||
} | ||
var agentName = "default" | ||
if config.AgentName != "" { | ||
agentName = config.AgentName | ||
} | ||
|
||
if config.AdvertisedPort == 0 { | ||
config.AdvertisedPort = config.Port | ||
} | ||
|
||
return &configData{ | ||
port: config.Port, | ||
advertisedPort: config.AdvertisedPort, | ||
hostName: config.HostName, | ||
agentName: agentName, | ||
}, nil | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have two sample configurations in this file....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed an issue with it. but the intention was for the second example to be specifically for the Proxies section... I can see how that could be confusing though. Maybe include "proxy" in the example string for it?