feat: ssh recording

This commit is contained in:
Johan Siebens
2024-05-31 08:24:55 +02:00
parent 78825d4e05
commit 4bce1c33b8
5 changed files with 117 additions and 15 deletions
+46
View File
@@ -1,6 +1,7 @@
package domain package domain
import ( import (
"fmt"
"github.com/jsiebens/ionscale/pkg/client/ionscale" "github.com/jsiebens/ionscale/pkg/client/ionscale"
"net/netip" "net/netip"
"strings" "strings"
@@ -48,6 +49,24 @@ func (a ACLPolicy) IsValidPeer(src *Machine, dest *Machine) bool {
} }
} }
for _, ssh := range a.SSH {
selfIps, otherIps := a.translateDestinationAliasesToMachineIPs(ssh.Recorder, dest)
if len(selfIps) != 0 {
for _, alias := range ssh.Destination {
if len(a.translateSourceAliasToMachineIPs(alias, src, &dest.User)) != 0 {
return true
}
}
}
if len(otherIps) != 0 {
for _, alias := range ssh.Destination {
if len(a.translateSourceAliasToMachineIPs(alias, src, nil)) != 0 {
return true
}
}
}
}
return false return false
} }
@@ -100,9 +119,36 @@ func (a ACLPolicy) BuildFilterRules(peers []Machine, dst *Machine) []tailcfg.Fil
rules = matchSourceAndAppendRule(rules, acl.Source, other, nil) rules = matchSourceAndAppendRule(rules, acl.Source, other, nil)
} }
for _, acl := range a.SSH {
ssh := a.prepareFilterRulesFromSSH(dst, acl)
rules = matchSourceAndAppendRule(rules, acl.Destination, ssh, nil)
}
return rules return rules
} }
func normalizeSSHRecordersToDestinationAliases(recorders []string) []string {
recorderAliases := make([]string, 0)
for _, alias := range recorders {
if strings.HasPrefix(alias, "tag:") {
recorderAliases = append(recorderAliases, fmt.Sprintf("%s:80", alias))
}
}
return recorderAliases
}
func (a ACLPolicy) prepareFilterRulesFromSSH(candidate *Machine, entry ionscale.ACLSSH) []tailcfg.FilterRule {
_, otherDstPorts := a.translateDestinationAliasesToMachineNetPortRanges(normalizeSSHRecordersToDestinationAliases(entry.Recorder), candidate)
var otherFilterRules []tailcfg.FilterRule
if len(otherDstPorts) != 0 {
otherFilterRules = append(otherFilterRules, tailcfg.FilterRule{IPProto: []int{protocolTCP}, DstPorts: otherDstPorts})
}
return otherFilterRules
}
func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ionscale.ACLEntry) ([]tailcfg.FilterRule, []tailcfg.FilterRule) { func (a ACLPolicy) prepareFilterRulesFromACL(candidate *Machine, acl ionscale.ACLEntry) ([]tailcfg.FilterRule, []tailcfg.FilterRule) {
proto := parseProtocol(acl.Protocol) proto := parseProtocol(acl.Protocol)
+26
View File
@@ -2,6 +2,7 @@ package domain
import ( import (
"github.com/jsiebens/ionscale/pkg/client/ionscale" "github.com/jsiebens/ionscale/pkg/client/ionscale"
"net/netip"
"strings" "strings"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@@ -29,6 +30,20 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
return result return result
} }
expandRecorderAliases := func(aliases []string) []netip.AddrPort {
result := make([]netip.AddrPort, 0)
for _, alias := range aliases {
for _, src := range append(srcs, *dst) {
if src.HasTag(alias) {
result = append(result, netip.AddrPortFrom(*src.IPv4.Addr, 80))
}
}
}
return result
}
for _, rule := range a.SSH { for _, rule := range a.SSH {
if rule.Action != "accept" && rule.Action != "check" { if rule.Action != "accept" && rule.Action != "check" {
continue continue
@@ -46,6 +61,17 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
} }
} }
if len(rule.Recorder) != 0 {
action.Recorders = expandRecorderAliases(rule.Recorder)
action.Message = "# This session is being recorded.\n"
if rule.EnforceRecorder {
action.OnRecordingFailure = &tailcfg.SSHRecorderFailureAction{
RejectSessionWithMessage: "# Session rejected: failed to start session recording.",
TerminateSessionWithMessage: "# Session terminated: failed to record session.",
}
}
}
selfUsers, otherUsers := a.expandSSHDstToSSHUsers(dst, rule) selfUsers, otherUsers := a.expandSSHDstToSSHUsers(dst, rule)
if len(selfUsers) != 0 { if len(selfUsers) != 0 {
-10
View File
@@ -1,8 +1,6 @@
package domain package domain
import ( import (
"encoding/json"
"fmt"
"github.com/jsiebens/ionscale/pkg/client/ionscale" "github.com/jsiebens/ionscale/pkg/client/ionscale"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -385,14 +383,6 @@ func TestACLPolicy_BuildSSHPolicy_WithTagsAndActionCheck(t *testing.T) {
assert.Nil(t, actualRules.Rules) assert.Nil(t, actualRules.Rules)
} }
func printRules(rules []*tailcfg.SSHRule) {
indent, err := json.MarshalIndent(rules, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(indent))
}
func sshPrincipalsFromMachines(machines ...Machine) []*tailcfg.SSHPrincipal { func sshPrincipalsFromMachines(machines ...Machine) []*tailcfg.SSHPrincipal {
x := StringSet{} x := StringSet{}
for _, m := range machines { for _, m := range machines {
+7 -5
View File
@@ -46,11 +46,13 @@ type ACLEntry struct {
} }
type ACLSSH struct { type ACLSSH struct {
Action string `json:"action,omitempty" hujson:"Action,omitempty"` Action string `json:"action,omitempty" hujson:"Action,omitempty"`
Users []string `json:"users,omitempty" hujson:"Users,omitempty"` Source []string `json:"src,omitempty" hujson:"Src,omitempty"`
Source []string `json:"src,omitempty" hujson:"Src,omitempty"` Destination []string `json:"dst,omitempty" hujson:"Dst,omitempty"`
Destination []string `json:"dst,omitempty" hujson:"Dst,omitempty"` Users []string `json:"users,omitempty" hujson:"Users,omitempty"`
CheckPeriod string `json:"checkPeriod,omitempty" hujson:"CheckPeriod,omitempty"` CheckPeriod string `json:"checkPeriod,omitempty" hujson:"CheckPeriod,omitempty"`
Recorder []string `json:"recorder,omitempty" hujson:"Recorder,omitempty"`
EnforceRecorder bool `json:"enforceRecorder,omitempty" hujson:"EnforceRecorder,omitempty"`
} }
type ACLNodeAttrGrant struct { type ACLNodeAttrGrant struct {
+38
View File
@@ -38,3 +38,41 @@ func TestACL_PeersShouldBeRemovedWhenNoMatchingACLRuleIsAvailable(t *testing.T)
require.NoError(t, server.WaitFor(tsn.PeerCount(0))) require.NoError(t, server.WaitFor(tsn.PeerCount(0)))
}) })
} }
func TestACL_PeersShouldSeeSSHRecorder(t *testing.T) {
sc.Run(t, func(s *sc.Scenario) {
tailnet := s.CreateTailnet()
clientKey := s.CreateAuthKey(tailnet.Id, true, "tag:client")
recorderKey := s.CreateAuthKey(tailnet.Id, true, "tag:recorder")
policy := defaults.DefaultACLPolicy()
policy.ACLs = []ionscale.ACLEntry{
{
Action: "accept",
Source: []string{"tag:client"},
Destination: []string{"tag:client:*"},
},
}
policy.SSH = []ionscale.ACLSSH{
{
Action: "check",
Source: []string{"tag:client"},
Destination: []string{"tag:client"},
Users: []string{"autogroup:nonroot", "root"},
Recorder: []string{"tag:recorder"},
},
}
s.SetACLPolicy(tailnet.Id, policy)
client1 := s.NewTailscaleNode()
client2 := s.NewTailscaleNode()
recorder1 := s.NewTailscaleNode()
require.NoError(t, client1.Up(clientKey))
require.NoError(t, client2.Up(clientKey))
require.NoError(t, recorder1.Up(recorderKey))
require.NoError(t, recorder1.WaitFor(tsn.PeerCount(2)))
})
}