Include 'internal_inbound' and 'internal_outbound' in 'vpn firewall show' output

This commit is contained in:
Brian Picciano 2024-12-31 11:46:49 +01:00
parent 3be64e913b
commit 5e692d38a5
7 changed files with 230 additions and 163 deletions

View File

@ -6,20 +6,21 @@ When providing resources on your host, whether
host's firewall is configured correctly to do so.
To make matters even more confusing, there are actually two firewalls at play:
the host's firewall, and the VPN firewall.
the host's firewall, and Isle's own VPN firewall.
## Host Firewall
The host you are running isle on will almost definitely have a firewall
running, separate from the VPN firewall. If you wish to provide services for
your Isle network from your host, you will need to allow their ports in your
host's firewall.
Your host's firewall filters all traffic across all network interfaces, while
Isle's VPN firewall filters traffic only across the network interfaces it
creates itself. This means there is some duplication of responsibility across
the two, and so configuring both is required for providing resources.
**isle does _not_ automatically configure your host's firewall to any extent!**
One option is to open your host to all traffic from your Isle network, and
allow the VPN firewall to be fully responsible for filtering traffic. To do this
on Linux using iptables, for example, you would add something like this to your
## Configuring the Host Firewall
By default Isle's VPN firewall will reject all inbound traffic on VPN
interfaces. This is a safe default, and so for simplicity it is recommended to
configure the host firewall to allow all traffic on Isle networks. To do this on
Linux using iptables, for example, you would add something like this to your
iptables configuration:
```
@ -33,32 +34,31 @@ you will need to manually determine and add the ports for each service to your
host's firewall. You will need to manually specify any configured storage
allocation ports if this is the approach you take.
## VPN Firewall
## Configuring the VPN Firewall
See the [Configuring Networks](./configuring-networks.md) document for notes on
how to configure Isle networks. This guide assumes configuration using the CLI.
Isle uses the [nebula][nebula] project to provide its VPN layer. Nebula ships
with its own [builtin firewall][nebulafirewall], which only applies to
connections coming in over the virtual network interface which it creates. This
connections coming in over the VPN interfaces which it creates for Isle. This
firewall can be manually configured using the `isle vpn firewall` set of
sub-commands, or using the [configuration file][configfile].
Any storage allocations which are defined will have their network ports
automatically added to the VPN firewall by Isle. This means that you only need
to configure the VPN firewall if you are hosting services for your isle network
besides storage.
[nebula]: https://github.com/slackhq/nebula
[nebulafirewall]: https://nebula.defined.net/docs/config/firewall
[configfile]: ./configuring-networks.md
### Configuring the VPN Firewall
See the [Configuring Networks](./configuring-networks.md) document for notes on
how to configure Isle networks. This guide assumes configuration using the CLI.
The `isle vpn firewall` sub-commands are used to configure the VPN's firewall.
Without any flags the `isle vpn firewall show` command will display the
currently active firewall.
Isle will automatically open inbound ports on its firewall for services it
provides, for example those necessary for storage allocations. When viewing open
ports using `isle vpn firewall show` these automatically opened ports will
appear separately under the `internal_inbound` section and are not configurable
by the user.
```bash
isle vpn firewall show
# outbound:
@ -75,6 +75,13 @@ isle vpn firewall show
# port: "22"
# proto: tcp
# host: my-laptop
# internal_inbound:
# - port: "3901"
# proto: tcp
# host: any
# - port: "3900"
# proto: tcp
# host: any
```
When making changes to the firewall, all changes are first applied to a staging

View File

@ -294,12 +294,20 @@ func newFirewallRuleViews(
type firewallView struct {
Outbound []firewallRuleView `yaml:"outbound"`
Inbound []firewallRuleView `yaml:"inbound"`
InternalOutbound []daecommon.ConfigFirewallRule `yaml:"internal_outbound,omitempty"`
InternalInbound []daecommon.ConfigFirewallRule `yaml:"internal_inbound,omitempty"`
}
func newFirewallView(firewallConfig daecommon.ConfigFirewall) firewallView {
func newFirewallView(networkConfig daecommon.NetworkConfig) firewallView {
var (
firewallConfig = networkConfig.VPN.Firewall
internalOutbound, internalInbound = networkConfig.InternalFirewallRules()
)
return firewallView{
Outbound: newFirewallRuleViews(firewallConfig.Outbound),
Inbound: newFirewallRuleViews(firewallConfig.Inbound),
InternalOutbound: internalOutbound,
InternalInbound: internalInbound,
}
}
@ -318,20 +326,6 @@ var subCmdVPNFirewallShow = subCmd{
return nil, fmt.Errorf("parsing flags: %w", err)
}
var (
firewallConfig daecommon.ConfigFirewall
foundStaged bool
)
if *staged {
var err error
if foundStaged, err = ctx.getChangeStager().get(
&firewallConfig, vpnFirewallConfigChangeStagerName,
); err != nil {
return nil, fmt.Errorf("checking for staged changes: %w", err)
}
}
if !foundStaged {
daemonRPC, err := ctx.newDaemonRPC()
if err != nil {
return nil, fmt.Errorf("creating daemon RPC client: %w", err)
@ -343,10 +337,20 @@ var subCmdVPNFirewallShow = subCmd{
return nil, fmt.Errorf("getting network config: %w", err)
}
firewallConfig = config.VPN.Firewall
if *staged {
var firewallConfig daecommon.ConfigFirewall
foundStaged, err := ctx.getChangeStager().get(
&firewallConfig, vpnFirewallConfigChangeStagerName,
)
if err != nil {
return nil, fmt.Errorf("checking for staged changes: %w", err)
} else if foundStaged {
config.VPN.Firewall = firewallConfig
}
}
return newFirewallView(firewallConfig), nil
return newFirewallView(config), nil
}),
}

View File

@ -7,8 +7,6 @@ import (
"isle/daemon/daecommon"
"isle/toolkit"
"os"
"slices"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -361,25 +359,43 @@ func TestVPNFirewallRemove(t *testing.T) {
func TestVPNFirewallShow(t *testing.T) {
t.Parallel()
emptyWant := map[string][]any{
"outbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "any",
"host": "any",
},
},
"inbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "icmp",
"host": "any",
},
},
}
tests := []struct {
name string
outbound, inbound []string
config daecommon.NetworkConfig
staged string
flags []string
want map[string][]any
}{
{
name: "empty",
want: map[string][]any{
"outbound": {},
"inbound": {},
},
want: emptyWant,
},
{
name: "single",
outbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
},
config: daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.Firewall.Outbound = []daecommon.ConfigFirewallRule{
{Port: "any", Proto: "icmp", Host: "any"},
}
}),
want: map[string][]any{
"outbound": {
map[string]any{
@ -389,18 +405,27 @@ func TestVPNFirewallShow(t *testing.T) {
"host": "any",
},
},
"inbound": {},
"inbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "icmp",
"host": "any",
},
},
},
},
{
name: "multiple",
outbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
},
inbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
`{"port":"22","proto":"tcp","host":"foo"}`,
},
config: daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.VPN.Firewall.Outbound = []daecommon.ConfigFirewallRule{
{Port: "any", Proto: "icmp", Host: "any"},
}
c.VPN.Firewall.Inbound = []daecommon.ConfigFirewallRule{
{Port: "any", Proto: "icmp", Host: "any"},
{Port: "22", Proto: "tcp", Host: "foo"},
}
}),
want: map[string][]any{
"outbound": {
map[string]any{
@ -428,27 +453,11 @@ func TestVPNFirewallShow(t *testing.T) {
},
{
name: "staged/nothing staged",
outbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
},
flags: []string{"--staged"},
want: map[string][]any{
"outbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "icmp",
"host": "any",
},
},
"inbound": {},
},
want: emptyWant,
},
{
name: "staged/staged but no flag",
outbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
},
staged: `{
"Inbound": [
{
@ -458,23 +467,10 @@ func TestVPNFirewallShow(t *testing.T) {
}
]
}`,
want: map[string][]any{
"outbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "icmp",
"host": "any",
},
},
"inbound": {},
},
want: emptyWant,
},
{
name: "staged/staged with flag",
outbound: []string{
`{"port":"any","proto":"icmp","host":"any"}`,
},
staged: `{
"Inbound": [
{
@ -497,17 +493,102 @@ func TestVPNFirewallShow(t *testing.T) {
},
},
},
{
name: "with alloc/no staged",
config: daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.Storage.Allocations = []daecommon.ConfigStorageAllocation{{
DataPath: "/data",
MetaPath: "/meta",
S3APIPort: 3901,
RPCPort: 3900,
AdminPort: 3902,
}}
}),
want: map[string][]any{
"outbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "any",
"host": "any",
},
},
"inbound": {
map[string]any{
"index": 0,
"port": "any",
"proto": "icmp",
"host": "any",
},
},
"internal_inbound": {
map[string]any{
"port": "3901",
"proto": "tcp",
"host": "any",
},
map[string]any{
"port": "3900",
"proto": "tcp",
"host": "any",
},
},
},
},
{
name: "with alloc/with staged",
config: daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
c.Storage.Allocations = []daecommon.ConfigStorageAllocation{{
DataPath: "/data",
MetaPath: "/meta",
S3APIPort: 3901,
RPCPort: 3900,
AdminPort: 3902,
}}
}),
staged: `{
"Inbound": [
{
"Port":"80",
"Proto":"tcp",
"Host":"some-host"
}
]
}`,
flags: []string{"--staged"},
want: map[string][]any{
"outbound": {},
"inbound": {
map[string]any{
"index": 0,
"port": "80",
"proto": "tcp",
"host": "some-host",
},
},
"internal_inbound": {
map[string]any{
"port": "3901",
"proto": "tcp",
"host": "any",
},
map[string]any{
"port": "3900",
"proto": "tcp",
"host": "any",
},
},
},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
h = newRunHarness(t)
config daecommon.NetworkConfig
h := newRunHarness(t)
outboundRawJSON = "[" + strings.Join(test.outbound, ",") + "]"
inboundRawJSON = "[" + strings.Join(test.inbound, ",") + "]"
)
h.daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(test.config, nil).
Once()
if test.staged != "" {
require.True(t, json.Valid([]byte(test.staged)))
@ -518,21 +599,6 @@ func TestVPNFirewallShow(t *testing.T) {
))
}
require.NoError(t, json.Unmarshal(
[]byte(outboundRawJSON), &config.VPN.Firewall.Outbound,
))
require.NoError(t, json.Unmarshal(
[]byte(inboundRawJSON), &config.VPN.Firewall.Inbound,
))
if !slices.Contains(test.flags, "--staged") || test.staged == "" {
h.daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
}
args := append([]string{"vpn", "firewall", "show"}, test.flags...)
h.runAssertStdout(t, test.want, args...)
})

View File

@ -10,7 +10,6 @@ import (
"isle/yamlutil"
"net"
"path/filepath"
"strconv"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
@ -89,21 +88,9 @@ func nebulaConfig(
)
firewall := networkConfig.VPN.Firewall
for _, alloc := range networkConfig.Storage.Allocations {
firewall.Inbound = append(
firewall.Inbound,
daecommon.ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
daecommon.ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
)
}
internalOutbound, internalInbound := networkConfig.InternalFirewallRules()
firewall.Outbound = append(firewall.Outbound, internalOutbound...)
firewall.Inbound = append(firewall.Inbound, internalInbound...)
type m = yamlutil.OrderedMap[string, any]

View File

@ -10,6 +10,7 @@ import (
"isle/yamlutil"
"net"
"sort"
"strconv"
_ "embed"
@ -121,6 +122,29 @@ func NewNetworkConfig(fn func(*NetworkConfig)) NetworkConfig {
return c
}
// InternalFirewallRules returns the firewall rules which should be added to the
// NetworkConfig automatically, beyond those which are managed by the user.
func (c *NetworkConfig) InternalFirewallRules() (
outbound, inbound []ConfigFirewallRule,
) {
for _, alloc := range c.Storage.Allocations {
inbound = append(
inbound,
ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
)
}
return
}
func (c *NetworkConfig) fillDefaults() {
if c.DNS.Resolvers == nil {
c.DNS.Resolvers = []string{

View File

@ -1,9 +0,0 @@
---
type: task
---
When listing firewall entries using `isle vpn firewall list`, any firewall
entries which are automatically included for garage (and all other services in
the future) should be included in the returned list as well. These should be
annotated in such a way that the user understands they are automatically
generated and can't be changed.

View File

@ -1,12 +0,0 @@
---
type: task
---
The Firewalls doc page should be extra clear that adding the line
```
-A INPUT --source <network CIDR> --jump ACCEPT
```
will not expose the host to the network entirely, as the nebula firewall will
still block all traffic by default.