mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
feat: ssh recording
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"net/netip"
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -100,9 +119,36 @@ func (a ACLPolicy) BuildFilterRules(peers []Machine, dst *Machine) []tailcfg.Fil
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
proto := parseProtocol(acl.Protocol)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package domain
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
@@ -29,6 +30,20 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
|
||||
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 {
|
||||
if rule.Action != "accept" && rule.Action != "check" {
|
||||
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)
|
||||
|
||||
if len(selfUsers) != 0 {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -385,14 +383,6 @@ func TestACLPolicy_BuildSSHPolicy_WithTagsAndActionCheck(t *testing.T) {
|
||||
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 {
|
||||
x := StringSet{}
|
||||
for _, m := range machines {
|
||||
|
||||
@@ -46,11 +46,13 @@ type ACLEntry struct {
|
||||
}
|
||||
|
||||
type ACLSSH struct {
|
||||
Action string `json:"action,omitempty" hujson:"Action,omitempty"`
|
||||
Users []string `json:"users,omitempty" hujson:"Users,omitempty"`
|
||||
Source []string `json:"src,omitempty" hujson:"Src,omitempty"`
|
||||
Destination []string `json:"dst,omitempty" hujson:"Dst,omitempty"`
|
||||
CheckPeriod string `json:"checkPeriod,omitempty" hujson:"CheckPeriod,omitempty"`
|
||||
Action string `json:"action,omitempty" hujson:"Action,omitempty"`
|
||||
Source []string `json:"src,omitempty" hujson:"Src,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"`
|
||||
Recorder []string `json:"recorder,omitempty" hujson:"Recorder,omitempty"`
|
||||
EnforceRecorder bool `json:"enforceRecorder,omitempty" hujson:"EnforceRecorder,omitempty"`
|
||||
}
|
||||
|
||||
type ACLNodeAttrGrant struct {
|
||||
|
||||
@@ -38,3 +38,41 @@ func TestACL_PeersShouldBeRemovedWhenNoMatchingACLRuleIsAvailable(t *testing.T)
|
||||
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)))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user