Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YAML support to ACLs #359

Merged
merged 6 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
- To limit access between nodes, use [ACLs](./docs/acls.md).

**Features**:

- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)

**Changes**:

- Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346)
Expand Down
40 changes: 31 additions & 9 deletions acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/rs/zerolog/log"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
"tailscale.com/tailcfg"
)
Expand Down Expand Up @@ -53,16 +55,36 @@ func (h *Headscale) LoadACLPolicy(path string) error {
return err
}

ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}
ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
switch filepath.Ext(path) {
case ".yml", ".yaml":
log.Debug().
Str("path", path).
Bytes("file", policyBytes).
Msg("Loading ACLs from YAML")

err := yaml.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}

log.Trace().
Interface("policy", policy).
Msg("Loaded policy from YAML")

default:
ast, err := hujson.Parse(policyBytes)
if err != nil {
return err
}

ast.Standardize()
policyBytes = ast.Pack()
err = json.Unmarshal(policyBytes, &policy)
if err != nil {
return err
}
}

if policy.IsZero() {
return errEmptyPolicy
}
Expand Down
16 changes: 16 additions & 0 deletions acls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,22 @@ func (s *Suite) TestPortWildcard(c *check.C) {
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}

func (s *Suite) TestPortWildcardYAML(c *check.C) {
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.yaml")
c.Assert(err, check.IsNil)

rules, err := app.generateACLRules()
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)

c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].SrcIPs, check.HasLen, 1)
c.Assert(rules[0].SrcIPs[0], check.Equals, "*")
}

func (s *Suite) TestPortNamespace(c *check.C) {
namespace, err := app.CreateNamespace("testnamespace")
c.Assert(err, check.IsNil)
Expand Down
44 changes: 33 additions & 11 deletions acls_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@ import (
"strings"

"github.com/tailscale/hujson"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
)

// ACLPolicy represents a Tailscale ACL Policy.
type ACLPolicy struct {
Groups Groups `json:"Groups"`
Hosts Hosts `json:"Hosts"`
TagOwners TagOwners `json:"TagOwners"`
ACLs []ACL `json:"ACLs"`
Tests []ACLTest `json:"Tests"`
Groups Groups `json:"Groups" yaml:"Groups"`
Hosts Hosts `json:"Hosts" yaml:"Hosts"`
TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"`
ACLs []ACL `json:"ACLs" yaml:"ACLs"`
Tests []ACLTest `json:"Tests" yaml:"Tests"`
}

// ACL is a basic rule for the ACL Policy.
type ACL struct {
Action string `json:"Action"`
Users []string `json:"Users"`
Ports []string `json:"Ports"`
Action string `json:"Action" yaml:"Action"`
Users []string `json:"Users" yaml:"Users"`
Ports []string `json:"Ports" yaml:"Ports"`
}

// Groups references a series of alias in the ACL rules.
Expand All @@ -35,9 +36,9 @@ type TagOwners map[string][]string

// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
type ACLTest struct {
User string `json:"User"`
Allow []string `json:"Allow"`
Deny []string `json:"Deny,omitempty"`
User string `json:"User" yaml:"User"`
Allow []string `json:"Allow" yaml:"Allow"`
Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"`
}

// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
Expand Down Expand Up @@ -69,6 +70,27 @@ func (hosts *Hosts) UnmarshalJSON(data []byte) error {
return nil
}

// UnmarshalYAML allows to parse the Hosts directly into netaddr objects.
func (hosts *Hosts) UnmarshalYAML(data []byte) error {
newHosts := Hosts{}
hostIPPrefixMap := make(map[string]string)

err := yaml.Unmarshal(data, &hostIPPrefixMap)
if err != nil {
return err
}
for host, prefixStr := range hostIPPrefixMap {
prefix, err := netaddr.ParseIPPrefix(prefixStr)
if err != nil {
return err
}
newHosts[host] = prefix
}
*hosts = newHosts

return nil
}

// IsZero is perhaps a bit naive here.
func (policy ACLPolicy) IsZero() bool {
if len(policy.Groups) == 0 && len(policy.Hosts) == 0 && len(policy.ACLs) == 0 {
Expand Down
3 changes: 2 additions & 1 deletion config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ tls_key_path: ""
log_level: info

# Path to a file containg ACL policies.
# Recommended path: /etc/headscale/acl.hujson
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: ""

## DNS
Expand Down
10 changes: 10 additions & 0 deletions tests/acls/acl_policy_basic_wildcards.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
Hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
ACLs:
- Action: accept
Users:
- "*"
Ports:
- host-1:*
8 changes: 0 additions & 8 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,6 @@ func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
var addressesSlices []string
h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices)

log.Trace().
Strs("addresses", addressesSlices).
Msg("Got allocated ip addresses from databases")

var ips netaddr.IPSetBuilder
for _, slice := range addressesSlices {
var machineAddresses MachineAddresses
Expand All @@ -216,10 +212,6 @@ func (h *Headscale) getUsedIPs() (*netaddr.IPSet, error) {
}
}

log.Trace().
Interface("addresses", ips).
Msg("Parsed ip addresses that has been allocated from databases")

ipSet, err := ips.IPSet()
if err != nil {
return &netaddr.IPSet{}, fmt.Errorf(
Expand Down