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
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user