Skip to content

Commit

Permalink
Add server and client implementations for MPA.
Browse files Browse the repository at this point in the history
These changes are sufficient for MPA when using a direct connection to the server. Here's a few sample commands you can run in parallel to try it out.

```
go run ./cmd/sansshell-server
go run ./cmd/sanssh -client-cert ./auth/mtls/testdata/client.pem -client-key ./auth/mtls/testdata/client.key -mpa -targets localhost healthcheck validate
go run ./cmd/sanssh -client-cert ./services/mpa/testdata/approver.pem -client-key ./services/mpa/testdata/approver.key -targets localhost mpa approve a59c2fef-748944da-336c9d35
```

I've added some new testdata certs because I'm forbidding cases where approver == requester. I've updated the sansshell server code to allow any request if it's requested by our "normal" client cert and approved by our "approver" client cert.

The output of `-mpa` prints a nonconfigurable help message to stderr while waiting on approval. If the command is already approved, the message won't show up.

```
$ sanssh -mpa -targets localhost healthcheck validate
Multi party auth requested, ask an approver to run:
  sanssh --targets localhost:50042 mpa approve a59c2fef-748944da-336c9d35
Target localhost:50042 (0) healthy`
```

This implements the client and server portion, but not the proxy portion. The proxy part mostly builds on top of what I have here and will take advantage of some other features I'm implementing.

- Snowflake-Labs#361 for implementing the proxy equivalent of `ServerMPAAuthzHook()`
- Snowflake-Labs#358 for implementing the proxy equivalents of `mpahooks.UnaryClientIntercepter()` and `mpahooks.StreamClientIntercepter()`
- Snowflake-Labs#359 so that MPA can use the identity of the caller to the proxy instead of the identity of the proxy.

I'm going to wait to mention this in the readme until I've implemented the proxy part.

Part of Snowflake-Labs#346
  • Loading branch information
stvnrhodes committed Oct 26, 2023
1 parent e39556e commit c4aadd4
Show file tree
Hide file tree
Showing 14 changed files with 1,352 additions and 1 deletion.
20 changes: 19 additions & 1 deletion auth/opa/rpcauth/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
)

// RPCAuthInput is used as policy input to validate Sansshell RPCs
// NOTE: RPCAuthInputForLogging must be updated when this changes.
type RPCAuthInput struct {
// The GRPC method name, as '/Package.Service/Method'
Method string `json:"method"`
Expand All @@ -52,6 +51,9 @@ type RPCAuthInput struct {
// Information about the host serving the RPC.
Host *HostAuthInput `json:"host"`

// Information about approvers when using multi-party authentication.
Approvers []*PrincipalAuthInput `json:"approvers"`

// Information about the environment in which the policy evaluation is
// happening.
Environment *EnvironmentInput `json:"environment"`
Expand Down Expand Up @@ -153,9 +155,25 @@ func NewRPCAuthInput(ctx context.Context, method string, req proto.Message) (*RP
return out, nil
}

type peerInfoKey struct{}

func AddPeerToContext(ctx context.Context, p *PeerAuthInput) context.Context {
if p == nil {
return ctx
}
return context.WithValue(ctx, peerInfoKey{}, p)
}

// PeerInputFromContext populates peer information from the supplied
// context, if available.
func PeerInputFromContext(ctx context.Context) *PeerAuthInput {
// If this runs after rpcauth hooks, we can return richer data that includes
// information added by the hooks.
cached, ok := ctx.Value(peerInfoKey{}).(*PeerAuthInput)
if ok {
return cached
}

out := &PeerAuthInput{}
p, ok := peer.FromContext(ctx)
if !ok {
Expand Down
7 changes: 7 additions & 0 deletions cmd/sanssh/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/Snowflake-Labs/sansshell/proxy/proxy"

cmdUtil "github.com/Snowflake-Labs/sansshell/cmd/util"
"github.com/Snowflake-Labs/sansshell/services/mpa/mpahooks"
"github.com/Snowflake-Labs/sansshell/services/util"
)

Expand Down Expand Up @@ -74,6 +75,8 @@ type RunState struct {
// BatchSize if non-zero will do the requested operation to the targets but in
// N calls to the proxy where N is the target list size divided by BatchSize.
BatchSize int
// If true, add an interceptor that performs the multi-party auth flow
EnableMPA bool
}

const (
Expand Down Expand Up @@ -317,6 +320,10 @@ func Run(ctx context.Context, rs RunState) {
streamInterceptors = append(streamInterceptors, clientAuthz.AuthorizeClientStream)
unaryInterceptors = append(unaryInterceptors, clientAuthz.AuthorizeClient)
}
if rs.EnableMPA {
unaryInterceptors = append(unaryInterceptors, mpahooks.UnaryClientIntercepter())
streamInterceptors = append(streamInterceptors, mpahooks.StreamClientIntercepter())
}
// timeout interceptor should be the last item in ops so that it's executed first.
streamInterceptors = append(streamInterceptors, StreamClientTimeoutInterceptor(rs.IdleTimeout))
unaryInterceptors = append(unaryInterceptors, UnaryClientTimeoutInterceptor(rs.IdleTimeout))
Expand Down
3 changes: 3 additions & 0 deletions cmd/sanssh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ If port is blank the default of %d will be used`, proxyEnv, defaultProxyPort))
verbosity = flag.Int("v", -1, "Verbosity level. > 0 indicates more extensive logging")
prefixHeader = flag.Bool("h", false, "If true prefix each line of output with '<index>-<target>: '")
batchSize = flag.Int("batch-size", 0, "If non-zero will perform the proxy->target work in batches of this size (with any remainder done at the end).")
mpa = flag.Bool("mpa", false, "Request multi-party approval for changes")

// targets will be bound to --targets for sending a single request to N nodes.
targetsFlag util.StringSliceCommaOrWhitespaceFlag
Expand Down Expand Up @@ -118,6 +119,7 @@ func init() {
subcommands.ImportantFlag("justification")
subcommands.ImportantFlag("client-policy")
subcommands.ImportantFlag("client-policy-file")
subcommands.ImportantFlag("mpa")
subcommands.ImportantFlag("v")
}

Expand Down Expand Up @@ -192,6 +194,7 @@ func main() {
ClientPolicy: clientPolicy,
PrefixOutput: *prefixHeader,
BatchSize: *batchSize,
EnableMPA: *mpa,
}
ctx := logr.NewContext(context.Background(), logger)

Expand Down
2 changes: 2 additions & 0 deletions cmd/sansshell-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import (
fdbserver "github.com/Snowflake-Labs/sansshell/services/fdb/server"
_ "github.com/Snowflake-Labs/sansshell/services/healthcheck/server"
_ "github.com/Snowflake-Labs/sansshell/services/localfile/server"
mpa "github.com/Snowflake-Labs/sansshell/services/mpa/server"
_ "github.com/Snowflake-Labs/sansshell/services/power/server"

// Packages needs a real import to bind flags.
Expand Down Expand Up @@ -171,6 +172,7 @@ func main() {
server.WithParsedPolicy(parsed),
server.WithJustification(*justification),
server.WithAuthzHook(rpcauth.PeerPrincipalFromCertHook()),
server.WithAuthzHook(mpa.ServerMPAAuthzHook()),
server.WithRawServerOption(func(s *grpc.Server) { reflection.Register(s) }),
server.WithRawServerOption(func(s *grpc.Server) { channelz.RegisterChannelzServiceToServer(s) }),
server.WithDebugPort(*debugport),
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/go-logr/stdr v1.2.2
github.com/google/go-cmp v0.6.0
github.com/google/subcommands v1.2.0
github.com/gowebpki/jcs v1.0.1
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0
github.com/open-policy-agent/opa v0.57.1
github.com/pkg/errors v0.9.1
Expand All @@ -21,6 +22,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.19.0
go.opentelemetry.io/otel/trace v1.19.0
gocloud.dev v0.32.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/sync v0.4.0
golang.org/x/sys v0.13.0
google.golang.org/grpc v1.59.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c4aadd4

Please sign in to comment.