Skip to content

Commit bb1e592

Browse files
committed
scaffold some basic match -> broker comms with net/rpc
1 parent 248c0c0 commit bb1e592

File tree

7 files changed

+186
-23
lines changed

7 files changed

+186
-23
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,43 @@ Actually, interestingly, we might be able to use openssh's ssh-agent to do this,
4343

4444
We should consider destination constraining the target host for these agents. Need to think about abuse vectors if we don't do that.
4545

46+
## Notes
47+
```mermaid
48+
sequenceDiagram
49+
box ssh invocation on a client
50+
participant ssh
51+
participant match
52+
participant broker
53+
end
54+
55+
box out on the internet
56+
participant ca
57+
participant policy
58+
end
59+
60+
ssh ->> match: Match exec ...
61+
match ->> broker: {matchdata}
62+
63+
create participant auth
64+
broker ->> auth: {state}
65+
66+
destroy auth
67+
auth ->> broker: {token, state, error}
68+
69+
broker ->> ca: {token, pubkey}
70+
ca ->> policy: {token, pubkey}
71+
policy ->> ca: {cert-params}
72+
ca ->> broker: {cert}
73+
74+
create participant agent
75+
broker ->> agent: create agent
76+
broker ->> match: {true/false, error}
77+
match ->> ssh: {true/false}
78+
ssh ->> agent: list keys
79+
agent ->> ssh: {cert, pubkey}
80+
ssh ->> agent: sign-with-cert
81+
```
82+
4683
## TODO
4784

4885
- **Implement a less strict netstring parser**: The current auth plugin protocol uses the `markdingo/netstring` library which strictly rejects whitespace between netstrings. This makes debugging auth plugins difficult since developers can't use `println()` for debugging output. We should implement a custom netstring parser that tolerates whitespace (spaces, tabs, `\n`, `\r`) between netstrings while still being strict about the netstring format itself. This would maintain protocol compatibility while significantly improving developer experience when writing auth plugins.

cmd/epithet/agent.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
)
66

77
type AgentCLI struct {
8-
Match []string `help:"Match patterns" short:"m" required:"true"`
9-
CaURL string `help:"CA URL" name:"ca-url" short:"c" required:"true"`
10-
Auth string `help:"Authentication command" short:"a" required:"true"`
8+
Match []string `help:"Match patterns" short:"m" required:"true"`
9+
CaURL string `help:"CA URL" name:"ca-url" short:"c" required:"true"`
10+
Auth string `help:"Authentication command" short:"a" required:"true"`
11+
Broker string `help:"Broker socket path" short:"b" require:"true"`
1112
}
1213

1314
func (a *AgentCLI) Run(logger *slog.Logger) error {
14-
logger.Debug("agent command received", "match", a.Match, "ca-url", a.CaURL, "auth", a.Auth)
15+
logger.Debug("agent command received", "agent", a)
1516
return nil
1617
}

pkg/agent/agent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ func (a *Agent) startAgentListener() error {
143143
return fmt.Errorf("unable to set permissions on agent socket: %w", err)
144144
}
145145
a.agentListener = agentListener
146-
go a.listenAndServeAgent(agentListener)
146+
go a.listenAndServeAgent()
147147
return nil
148148
}
149149

150-
func (a *Agent) listenAndServeAgent(listener net.Listener) {
150+
func (a *Agent) listenAndServeAgent() {
151151
for a.Running() {
152-
conn, err := listener.Accept()
152+
conn, err := a.agentListener.Accept()
153153
if err != nil {
154154
if conn != nil {
155155
conn.Close()

pkg/broker/auth.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ type Auth struct {
3333
state []byte
3434
}
3535

36-
// New creates a new Auth with an unparsed command line.
37-
func New(cmdLine string) *Auth {
36+
// NewAuth creates a new Auth with an unparsed command line.
37+
func NewAuth(cmdLine string) *Auth {
3838
return &Auth{
3939
cmdLine: cmdLine,
4040
state: []byte{},

pkg/broker/auth_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func TestDecodeAuthOutput_NeitherTokenNorError(t *testing.T) {
176176
}
177177

178178
func TestAuth_New(t *testing.T) {
179-
auth := New("my-auth-command --flag")
179+
auth := NewAuth("my-auth-command --flag")
180180
require.NotNil(t, auth)
181181
require.Equal(t, "my-auth-command --flag", auth.cmdLine)
182182
require.Empty(t, auth.state)
@@ -192,7 +192,7 @@ cat > /dev/null
192192
printf '%s' "11:tmy-token-1,"
193193
`)
194194

195-
auth := New(script)
195+
auth := NewAuth(script)
196196
token, err := auth.Run(nil)
197197
require.NoError(t, err)
198198
require.Equal(t, "my-token-1", token)
@@ -209,7 +209,7 @@ printf '%s' "11:tmy-token-2,"
209209
printf '%s' "18:s{\"refresh\":\"xyz\"},"
210210
`)
211211

212-
auth := New(script)
212+
auth := NewAuth(script)
213213
token, err := auth.Run(nil)
214214
require.NoError(t, err)
215215
require.Equal(t, "my-token-2", token)
@@ -224,7 +224,7 @@ printf '%s' "6:ttoken,"
224224
printf '%s' "12:s{\"count\":1},"
225225
`)
226226

227-
auth := New(script1)
227+
auth := NewAuth(script1)
228228
token, err := auth.Run(nil)
229229
require.NoError(t, err)
230230
require.Equal(t, "token", token)
@@ -255,7 +255,7 @@ cat > /dev/null
255255
printf '%s' "22:eAuthentication failed,"
256256
`)
257257

258-
auth := New(script)
258+
auth := NewAuth(script)
259259
_, err := auth.Run(nil)
260260
require.Error(t, err)
261261
require.Equal(t, "Authentication failed", err.Error())
@@ -267,7 +267,7 @@ echo "Something went wrong" >&2
267267
exit 1
268268
`)
269269

270-
auth := New(script)
270+
auth := NewAuth(script)
271271
_, err := auth.Run(nil)
272272
require.Error(t, err)
273273
require.Contains(t, err.Error(), "auth command failed")
@@ -280,7 +280,7 @@ cat > /dev/null
280280
printf '%s' "not a valid netstring"
281281
`)
282282

283-
auth := New(script)
283+
auth := NewAuth(script)
284284
_, err := auth.Run(nil)
285285
require.Error(t, err)
286286
require.Contains(t, err.Error(), "failed to decode auth output")
@@ -289,14 +289,14 @@ printf '%s' "not a valid netstring"
289289
func TestAuth_Run_MustacheTemplateRendering(t *testing.T) {
290290
// Test that mustache template is rendered in command line
291291
// Template renders "ok" (2 chars) so "token-ok" is 8 chars, plus 't' key = 9 total
292-
auth := New(`printf '%s' "9:ttoken-{{host}},"`)
292+
auth := NewAuth(`printf '%s' "9:ttoken-{{host}},"`)
293293
token, err := auth.Run(map[string]string{"host": "ok"})
294294
require.NoError(t, err)
295295
require.Equal(t, "token-ok", token)
296296
}
297297

298298
func TestAuth_Run_MustacheTemplateError(t *testing.T) {
299-
auth := New("echo {{unclosed}")
299+
auth := NewAuth("echo {{unclosed}")
300300
_, err := auth.Run(nil)
301301
require.Error(t, err)
302302
require.Contains(t, err.Error(), "failed to render command template")
@@ -311,7 +311,7 @@ printf '%s' "6:ttoken,"
311311
printf '%s' "12:s{\"count\":1},"
312312
`)
313313

314-
auth := New(script)
314+
auth := NewAuth(script)
315315

316316
// Run two calls concurrently
317317
done := make(chan bool, 2)
@@ -355,7 +355,7 @@ else
355355
fi
356356
`)
357357

358-
auth := New(script)
358+
auth := NewAuth(script)
359359
token, err := auth.Run(nil)
360360
require.NoError(t, err)
361361
require.Equal(t, "first-call-token", token)
@@ -369,7 +369,7 @@ printf '%s' "6:ttoken,"
369369
# No state field - should clear existing state
370370
`)
371371

372-
auth := New(script)
372+
auth := NewAuth(script)
373373
auth.state = []byte("old-state") // Set some initial state
374374

375375
token, err := auth.Run(nil)
@@ -418,7 +418,7 @@ else
418418
fi
419419
`)
420420

421-
auth := New(script1)
421+
auth := NewAuth(script1)
422422
token, err := auth.Run(map[string]string{"user": "alice"})
423423
require.NoError(t, err)
424424
require.Equal(t, "initial-auth-token", token)

pkg/broker/broker.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,96 @@
11
package broker
22

3-
import "sync"
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"net"
8+
"net/rpc"
9+
"os"
10+
"sync"
11+
)
412

513
type Broker struct {
614
lock sync.Mutex
715
done chan struct{}
816
closeOnce sync.Once
17+
log slog.Logger
18+
19+
brokerSocketPath string
20+
brokerListener net.Listener
21+
auth *Auth
22+
}
23+
24+
type MatchRequest struct {
25+
LocalHost string
26+
LocalUser string
27+
RemoteHost string
28+
RemoteUser string
29+
ConnectionHash string
30+
// TODO fill in the rest of the %C fields
31+
}
32+
33+
type MatchResponse struct {
34+
// Should the `Match exec` actually match?
35+
Allow bool
36+
37+
// Error contains any error which should be reported to the user on stderr
38+
Error string
39+
}
40+
41+
func New(ctx context.Context, log slog.Logger, socketPath string, authCommand string) (*Broker, error) {
42+
b := Broker{
43+
auth: NewAuth(authCommand),
44+
brokerSocketPath: socketPath,
45+
done: make(chan struct{}),
46+
}
47+
48+
err := b.startBrokerListener()
49+
if err != nil {
50+
return nil, fmt.Errorf("Unable to start broker socket: %w", err)
51+
}
52+
53+
context.AfterFunc(ctx, func() {
54+
b.Close()
55+
})
56+
return &b, nil
57+
}
58+
59+
func (b *Broker) startBrokerListener() error {
60+
_ = os.Remove(b.brokerSocketPath) // Remove socket if it exists
61+
brokerListener, err := net.Listen("unix", b.brokerSocketPath)
62+
if err != nil {
63+
return fmt.Errorf("unable to start broker listener: %w", err)
64+
}
65+
66+
b.brokerListener = brokerListener
67+
go b.listenAndServe()
68+
return nil
69+
}
70+
71+
// Match is invoked via rpc from `epithet match` invocations
72+
func (b *Broker) Match(input MatchRequest, output *MatchResponse) error {
73+
output.Allow = true
74+
return nil
75+
}
76+
77+
func (b *Broker) listenAndServe() {
78+
server := rpc.NewServer()
79+
server.Register(b)
80+
for {
81+
conn, err := b.brokerListener.Accept()
82+
if err != nil {
83+
b.log.Warn("Unable to accept connection", "error", err)
84+
if b.Running() {
85+
continue
86+
} else {
87+
// shutting down, just exit
88+
return
89+
}
90+
}
91+
defer conn.Close()
92+
go server.ServeConn(conn)
93+
}
994
}
1095

1196
func (b *Broker) Done() <-chan struct{} {
@@ -14,6 +99,7 @@ func (b *Broker) Done() <-chan struct{} {
1499

15100
func (b *Broker) Close() {
16101
b.closeOnce.Do(func() {
102+
_ = b.brokerListener.Close()
17103
close(b.done)
18104
})
19105
}

pkg/broker/broker_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package broker
2+
3+
import (
4+
"log/slog"
5+
"net/rpc"
6+
"testing"
7+
8+
"github.com/lmittmann/tint"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_RpcBasics(t *testing.T) {
13+
ctx := t.Context()
14+
authCommand := "echo '6:thello,'"
15+
b, err := New(
16+
ctx,
17+
*testLogger(t),
18+
"/tmp/foooo", // TODO replace with a tempfile location which we cleanup
19+
authCommand)
20+
require.NoError(t, err)
21+
defer b.Close()
22+
23+
client, err := rpc.Dial("unix", "/tmp/foooo")
24+
require.NoError(t, err)
25+
26+
resp := MatchResponse{}
27+
err = client.Call("Broker.Match", MatchRequest{}, &resp)
28+
require.NoError(t, err)
29+
30+
require.True(t, resp.Allow)
31+
}
32+
33+
func testLogger(t *testing.T) *slog.Logger {
34+
logger := slog.New(tint.NewHandler(t.Output(), &tint.Options{
35+
Level: slog.LevelDebug,
36+
TimeFormat: "15:04:05",
37+
}))
38+
return logger
39+
}

0 commit comments

Comments
 (0)