tests/firewall_test.go
Rama master · réplica de lectura del repositorio original.
package main
import (
"context"
"net/http"
"regexp"
"testing"
"github.com/shellhub-io/shellhub/pkg/models"
"github.com/shellhub-io/shellhub/tests/environment"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
const (
firewallBlockedLog = "destination device is blocked by a firewall rule"
errFirewallRuleBlock = "because a firewall rule block your connection"
anyValue = ".*"
)
var anyDevice = environment.FirewallRuleFilter{Hostname: anyValue}
func TestEnterpriseFirewall(t *testing.T) {
t.Parallel()
ctx := context.Background()
compose := newEnterpriseEnvironment(t, ctx, environment.New(t, run))
signer := registerDeviceKey(t, ctx, compose)
_, device := startAcceptedAgent(t, ctx, compose)
sshid := deviceSSHID(device)
source := regexp.QuoteMeta(clientAddress(t, compose, sshid, signer))
hostname := environment.FirewallRuleFilter{Hostname: regexp.QuoteMeta(device.Name)}
denyAll := deny(100, matching(anyValue, anyValue, anyDevice))
createTag(t, compose, "firewalled")
createTag(t, compose, "elsewhere")
tagDevice(t, compose, device.UID, "firewalled")
t.Run("a connection is let through when the namespace has no rules", func(t *testing.T) {
require.Zero(t, compose.FirewallRuleCount(t))
requireKeyLogsIn(t, compose, sshid, signer)
})
t.Run("an allow rule matching the source address, the user and the hostname lets the connection through", func(t *testing.T) {
addFirewallRule(t, compose, denyAll)
addFirewallRule(t, compose, allow(1, matching(source, ShellHubAgentUsername, hostname)))
requireKeyLogsIn(t, compose, sshid, signer)
})
denials := []struct {
description string
rule environment.FirewallRule
}{
{description: "source address", rule: matching(source, anyValue, anyDevice)},
{description: "user", rule: matching(anyValue, ShellHubAgentUsername, anyDevice)},
{description: "hostname", rule: matching(anyValue, anyValue, hostname)},
{description: "tag", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"firewalled"}})},
}
for _, tc := range denials {
t.Run("a deny rule matching the "+tc.description+" blocks the connection", func(t *testing.T) {
addFirewallRule(t, compose, deny(1, tc.rule))
requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
})
}
misses := []struct {
description string
rule environment.FirewallRule
}{
{description: "another source address", rule: matching(`192\.0\.2\.1`, anyValue, anyDevice)},
{description: "another user", rule: matching(anyValue, "nobody", anyDevice)},
{description: "another hostname", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Hostname: "elsewhere"})},
{description: "a tag the device does not carry", rule: matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"elsewhere"}})},
}
for _, tc := range misses {
t.Run("a deny rule for "+tc.description+" lets the connection through", func(t *testing.T) {
addFirewallRule(t, compose, deny(1, tc.rule))
requireKeyLogsIn(t, compose, sshid, signer)
})
}
t.Run("an allow rule with a lower priority value wins over a matching deny rule", func(t *testing.T) {
addFirewallRule(t, compose, denyAll)
addFirewallRule(t, compose, allow(1, matching(anyValue, anyValue, anyDevice)))
requireKeyLogsIn(t, compose, sshid, signer)
})
t.Run("a deny rule with a lower priority value wins over a matching allow rule", func(t *testing.T) {
addFirewallRule(t, compose, allow(200, matching(anyValue, anyValue, anyDevice)))
addFirewallRule(t, compose, denyAll)
requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
})
t.Run("an inactive deny rule is skipped", func(t *testing.T) {
inactive := denyAll
inactive.Active = false
addFirewallRule(t, compose, inactive)
requireKeyLogsIn(t, compose, sshid, signer)
})
t.Run("a renamed tag keeps the rule matching the devices carrying it", func(t *testing.T) {
createTag(t, compose, "rulebefore")
t.Cleanup(func() { deleteTag(t, compose, "ruleafter") })
attachTag(t, compose, device.UID, "rulebefore")
rule := addFirewallRule(t, compose, deny(1, matching(anyValue, anyValue, environment.FirewallRuleFilter{Tags: []string{"rulebefore"}})))
renameTag(t, compose, "rulebefore", "ruleafter")
assert.Equal(t, []string{"ruleafter"}, firewallRuleTags(t, compose, rule))
requireAccessDenied(t, compose, sshid, signer, firewallBlockedLog, errFirewallRuleBlock)
})
t.Run("an upgraded namespace is not held to its firewall rules", func(t *testing.T) {
compose.UpgradeNamespace(t, ShellHubNamespace)
owner, _ := enrollOwnerKey(t, compose)
addFirewallRule(t, compose, denyAll)
requireStraightThrough(t, compose, sshid, owner)
})
}
func matching(sourceIP, username string, filter environment.FirewallRuleFilter) environment.FirewallRule {
return environment.FirewallRule{Active: true, SourceIP: sourceIP, Username: username, Filter: filter}
}
func allow(priority int, rule environment.FirewallRule) environment.FirewallRule {
rule.Priority = priority
rule.Action = environment.FirewallAllow
return rule
}
func deny(priority int, rule environment.FirewallRule) environment.FirewallRule {
rule.Priority = priority
rule.Action = environment.FirewallDeny
return rule
}
func addFirewallRule(t *testing.T, compose *environment.DockerCompose, rule environment.FirewallRule) string {
t.Helper()
id := compose.CreateFirewallRule(t, rule)
t.Cleanup(func() { compose.DeleteFirewallRule(t, id) })
return id
}
func firewallRuleTags(t *testing.T, compose *environment.DockerCompose, id string) []string {
t.Helper()
rule := struct {
Filter struct {
Tags []models.Tag `json:"tags"`
} `json:"filter"`
}{}
resp, err := compose.R(t.Context()).SetResult(&rule).Get("/api/firewall/rules/" + id)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
return tagNames(rule.Filter.Tags)
}
func clientAddress(t *testing.T, compose *environment.DockerCompose, sshid string, signer ssh.Signer) string {
t.Helper()
requireKeyLogsIn(t, compose, sshid, signer)
sessions := []models.Session{}
resp, err := compose.R(t.Context()).SetResult(&sessions).Get("/api/sessions")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
require.NotEmpty(t, sessions)
require.NotEmpty(t, sessions[0].IPAddress)
return sessions[0].IPAddress
}