mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-18 21:37:25 +00:00
Compare commits
39 Commits
v0.16.0-be
...
acl-syntax
Author | SHA1 | Date | |
---|---|---|---|
![]() |
569f3caab9 | ||
![]() |
7cd0f5e8a4 | ||
![]() |
f93cf4b980 | ||
![]() |
3d7be5b287 | ||
![]() |
cdf41bd500 | ||
![]() |
735a6aaa39 | ||
![]() |
0c2648c188 | ||
![]() |
7e6291c21c | ||
![]() |
586c5411f1 | ||
![]() |
2be16b581c | ||
![]() |
06e22bf878 | ||
![]() |
0b4b530809 | ||
![]() |
efca3daa5c | ||
![]() |
fdefe46c40 | ||
![]() |
80ad1db228 | ||
![]() |
e918ea89a3 | ||
![]() |
19b968849f | ||
![]() |
5bc11891f5 | ||
![]() |
818d26b5f9 | ||
![]() |
c47354bdc3 | ||
![]() |
86ce0e0c66 | ||
![]() |
39f03b86c8 | ||
![]() |
8287ba24b9 | ||
![]() |
ab1aac9f3e | ||
![]() |
3e353004b8 | ||
![]() |
bcb04d38a5 | ||
![]() |
de0e2bf828 | ||
![]() |
8fed47a2be | ||
![]() |
17d4968425 | ||
![]() |
54acee6880 | ||
![]() |
a4e05d4db3 | ||
![]() |
0c5a402206 | ||
![]() |
8744eeeb19 | ||
![]() |
ce13596077 | ||
![]() |
402a29e50c | ||
![]() |
0363e58467 | ||
![]() |
c8a14ccabb | ||
![]() |
1de29fd4e6 | ||
![]() |
75a0155f73 |
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## 0.16.0 (2022-xx-xx)
|
## 0.16.0 (2022-xx-xx)
|
||||||
|
|
||||||
|
### BREAKING
|
||||||
|
|
||||||
|
- Old ACL syntax is no longer supported ("users" & "ports" -> "src" & "dst"). Please check [the new syntax](https://tailscale.com/kb/1018/acls/).
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
|
- **Drop** armhf (32-bit ARM) support. [#609](https://github.com/juanfont/headscale/pull/609)
|
||||||
@@ -22,6 +26,8 @@
|
|||||||
- This change disables the logs by default
|
- This change disables the logs by default
|
||||||
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
|
- Use [Prometheus]'s duration parser, supporting days (`d`), weeks (`w`) and years (`y`) [#598](https://github.com/juanfont/headscale/pull/598)
|
||||||
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
|
- Add support for reloading ACLs with SIGHUP [#601](https://github.com/juanfont/headscale/pull/601)
|
||||||
|
- Use new ACL syntax [#618](https://github.com/juanfont/headscale/pull/618)
|
||||||
|
- Add -c option to specify config file from command line [#285](https://github.com/juanfont/headscale/issues/285) [#612](https://github.com/juanfont/headscale/pull/601)
|
||||||
|
|
||||||
## 0.15.0 (2022-03-20)
|
## 0.15.0 (2022-03-20)
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN strip /go/bin/headscale
|
RUN strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN strip /go/bin/headscale
|
RUN strip /go/bin/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
RUN CGO_ENABLED=0 GOOS=linux go install -a ./cmd/headscale
|
||||||
RUN test -e /go/bin/headscale
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
# Debug image
|
# Debug image
|
||||||
|
32
README.md
32
README.md
@@ -415,6 +415,15 @@ make build
|
|||||||
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
<sub style="font-size:14px"><b> Carson Yang</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/kundel>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/10158899?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kundel/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>kundel</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/fkr>
|
<a href=https://github.com/fkr>
|
||||||
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
|
<img src=https://avatars.githubusercontent.com/u/51063?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Kronlage-Dammers/>
|
||||||
@@ -422,8 +431,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
<sub style="font-size:14px"><b>Felix Kronlage-Dammers</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/felixonmars>
|
<a href=https://github.com/felixonmars>
|
||||||
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
|
<img src=https://avatars.githubusercontent.com/u/1006477?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Felix Yan/>
|
||||||
@@ -445,6 +452,13 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
<sub style="font-size:14px"><b>Jamie Greeff</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
|
<a href=https://github.com/huskyii>
|
||||||
|
<img src=https://avatars.githubusercontent.com/u/5499746?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jiang Zhu/>
|
||||||
|
<br />
|
||||||
|
<sub style="font-size:14px"><b>Jiang Zhu</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/jimt>
|
<a href=https://github.com/jimt>
|
||||||
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
<img src=https://avatars.githubusercontent.com/u/180326?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jim Tittsler/>
|
||||||
@@ -452,6 +466,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
<sub style="font-size:14px"><b>Jim Tittsler</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/piec>
|
<a href=https://github.com/piec>
|
||||||
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
<img src=https://avatars.githubusercontent.com/u/781471?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Pierre Carru/>
|
||||||
@@ -466,8 +482,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
<sub style="font-size:14px"><b>rcursaru</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/renovate-bot>
|
<a href=https://github.com/renovate-bot>
|
||||||
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
<img src=https://avatars.githubusercontent.com/u/25180681?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=WhiteSource Renovate/>
|
||||||
@@ -496,6 +510,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Tanner</b></sub>
|
<sub style="font-size:14px"><b>Tanner</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Teteros>
|
<a href=https://github.com/Teteros>
|
||||||
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
|
<img src=https://avatars.githubusercontent.com/u/5067989?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Teteros/>
|
||||||
@@ -510,8 +526,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
|
<sub style="font-size:14px"><b>The Gitter Badger</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/tianon>
|
<a href=https://github.com/tianon>
|
||||||
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
|
<img src=https://avatars.githubusercontent.com/u/161631?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Tianon Gravi/>
|
||||||
@@ -540,6 +554,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
<sub style="font-size:14px"><b>Zakhar Bessarab</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Bpazy>
|
<a href=https://github.com/Bpazy>
|
||||||
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
|
<img src=https://avatars.githubusercontent.com/u/9838749?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=ZiYuan/>
|
||||||
@@ -554,8 +570,6 @@ make build
|
|||||||
<sub style="font-size:14px"><b>derelm</b></sub>
|
<sub style="font-size:14px"><b>derelm</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/nning>
|
<a href=https://github.com/nning>
|
||||||
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
|
<img src=https://avatars.githubusercontent.com/u/557430?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=henning mueller/>
|
||||||
@@ -584,6 +598,8 @@ make build
|
|||||||
<sub style="font-size:14px"><b>pernila</b></sub>
|
<sub style="font-size:14px"><b>pernila</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/Wakeful-Cloud>
|
<a href=https://github.com/Wakeful-Cloud>
|
||||||
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
<img src=https://avatars.githubusercontent.com/u/38930607?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Wakeful-Cloud/>
|
||||||
|
107
acls.go
107
acls.go
@@ -23,6 +23,7 @@ const (
|
|||||||
errInvalidGroup = Error("invalid group")
|
errInvalidGroup = Error("invalid group")
|
||||||
errInvalidTag = Error("invalid tag")
|
errInvalidTag = Error("invalid tag")
|
||||||
errInvalidPortFormat = Error("invalid port format")
|
errInvalidPortFormat = Error("invalid port format")
|
||||||
|
errWildcardIsNeeded = Error("wildcard as port is required for the protocol")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,6 +37,23 @@ const (
|
|||||||
expectedTokenItems = 2
|
expectedTokenItems = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// For some reason golang.org/x/net/internal/iana is an internal package
|
||||||
|
const (
|
||||||
|
protocolICMP = 1 // Internet Control Message
|
||||||
|
protocolIGMP = 2 // Internet Group Management
|
||||||
|
protocolIPv4 = 4 // IPv4 encapsulation
|
||||||
|
protocolTCP = 6 // Transmission Control
|
||||||
|
protocolEGP = 8 // Exterior Gateway Protocol
|
||||||
|
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
|
||||||
|
protocolUDP = 17 // User Datagram
|
||||||
|
protocolGRE = 47 // Generic Routing Encapsulation
|
||||||
|
protocolESP = 50 // Encap Security Payload
|
||||||
|
protocolAH = 51 // Authentication Header
|
||||||
|
protocolIPv6ICMP = 58 // ICMP for IPv6
|
||||||
|
protocolSCTP = 132 // Stream Control Transmission Protocol
|
||||||
|
ProtocolFC = 133 // Fibre Channel
|
||||||
|
)
|
||||||
|
|
||||||
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
|
// LoadACLPolicy loads the ACL policy from the specify path, and generates the ACL rules.
|
||||||
func (h *Headscale) LoadACLPolicy(path string) error {
|
func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
@@ -123,23 +141,31 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, user := range acl.Users {
|
for innerIndex, src := range acl.Sources {
|
||||||
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Source %d", index, innerIndex)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
srcIPs = append(srcIPs, srcs...)
|
srcIPs = append(srcIPs, srcs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocols, needsWildcard, err := parseProtocol(acl.Protocol)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Msgf("Error parsing ACL %d. protocol unknown %s", index, acl.Protocol)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for innerIndex, ports := range acl.Ports {
|
for innerIndex, dest := range acl.Destinations {
|
||||||
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
dests, err := h.generateACLPolicyDest(machines, *h.aclPolicy, dest, needsWildcard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Destination %d", index, innerIndex)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -149,6 +175,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
rules = append(rules, tailcfg.FilterRule{
|
rules = append(rules, tailcfg.FilterRule{
|
||||||
SrcIPs: srcIPs,
|
SrcIPs: srcIPs,
|
||||||
DstPorts: destPorts,
|
DstPorts: destPorts,
|
||||||
|
IPProto: protocols,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +185,18 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
|||||||
func (h *Headscale) generateACLPolicySrcIP(
|
func (h *Headscale) generateACLPolicySrcIP(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
u string,
|
src string,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
|
return expandAlias(machines, aclPolicy, src, h.cfg.OIDC.StripEmaildomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicyDestPorts(
|
func (h *Headscale) generateACLPolicyDest(
|
||||||
machines []Machine,
|
machines []Machine,
|
||||||
aclPolicy ACLPolicy,
|
aclPolicy ACLPolicy,
|
||||||
d string,
|
dest string,
|
||||||
|
needsWildcard bool,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(d, ":")
|
tokens := strings.Split(dest, ":")
|
||||||
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
if len(tokens) < expectedTokenItems || len(tokens) > 3 {
|
||||||
return nil, errInvalidPortFormat
|
return nil, errInvalidPortFormat
|
||||||
}
|
}
|
||||||
@@ -195,7 +223,7 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ports, err := expandPorts(tokens[len(tokens)-1])
|
ports, err := expandPorts(tokens[len(tokens)-1], needsWildcard)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -214,6 +242,54 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
|||||||
return dests, nil
|
return dests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseProtocol reads the proto field of the ACL and generates a list of
|
||||||
|
// protocols that will be allowed, following the IANA IP protocol number
|
||||||
|
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||||
|
//
|
||||||
|
// If the ACL proto field is empty, it allows ICMPv4, ICMPv6, TCP, and UDP,
|
||||||
|
// as per Tailscale behaviour (see tailcfg.FilterRule).
|
||||||
|
//
|
||||||
|
// Also returns a boolean indicating if the protocol
|
||||||
|
// requires all the destinations to use wildcard as port number (only TCP,
|
||||||
|
// UDP and SCTP support specifying ports).
|
||||||
|
func parseProtocol(protocol string) ([]int, bool, error) {
|
||||||
|
switch protocol {
|
||||||
|
case "":
|
||||||
|
return []int{protocolICMP, protocolIPv6ICMP, protocolTCP, protocolUDP}, false, nil
|
||||||
|
case "igmp":
|
||||||
|
return []int{protocolIGMP}, true, nil
|
||||||
|
case "ipv4", "ip-in-ip":
|
||||||
|
return []int{protocolIPv4}, true, nil
|
||||||
|
case "tcp":
|
||||||
|
return []int{protocolTCP}, false, nil
|
||||||
|
case "egp":
|
||||||
|
return []int{protocolEGP}, true, nil
|
||||||
|
case "igp":
|
||||||
|
return []int{protocolIGP}, true, nil
|
||||||
|
case "udp":
|
||||||
|
return []int{protocolUDP}, false, nil
|
||||||
|
case "gre":
|
||||||
|
return []int{protocolGRE}, true, nil
|
||||||
|
case "esp":
|
||||||
|
return []int{protocolESP}, true, nil
|
||||||
|
case "ah":
|
||||||
|
return []int{protocolAH}, true, nil
|
||||||
|
case "sctp":
|
||||||
|
return []int{protocolSCTP}, false, nil
|
||||||
|
case "icmp":
|
||||||
|
return []int{protocolICMP, protocolIPv6ICMP}, true, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
protocolNumber, err := strconv.Atoi(protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
needsWildcard := protocolNumber != protocolTCP && protocolNumber != protocolUDP && protocolNumber != protocolSCTP
|
||||||
|
|
||||||
|
return []int{protocolNumber}, needsWildcard, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// expandalias has an input of either
|
// expandalias has an input of either
|
||||||
// - a namespace
|
// - a namespace
|
||||||
// - a group
|
// - a group
|
||||||
@@ -268,6 +344,7 @@ func expandAlias(
|
|||||||
alias,
|
alias,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ips, nil
|
return ips, nil
|
||||||
} else {
|
} else {
|
||||||
return ips, err
|
return ips, err
|
||||||
@@ -359,13 +436,17 @@ func excludeCorrectlyTaggedNodes(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
func expandPorts(portsStr string, needsWildcard bool) (*[]tailcfg.PortRange, error) {
|
||||||
if portsStr == "*" {
|
if portsStr == "*" {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needsWildcard {
|
||||||
|
return nil, errWildcardIsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
ports := []tailcfg.PortRange{}
|
ports := []tailcfg.PortRange{}
|
||||||
for _, portStr := range strings.Split(portsStr, ",") {
|
for _, portStr := range strings.Split(portsStr, ",") {
|
||||||
rang := strings.Split(portStr, "-")
|
rang := strings.Split(portStr, "-")
|
||||||
|
79
acls_test.go
79
acls_test.go
@@ -62,7 +62,7 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
|||||||
func (s *Suite) TestInvalidAction(c *check.C) {
|
func (s *Suite) TestInvalidAction(c *check.C) {
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
|
{Action: "invalidAction", Sources: []string{"*"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -70,14 +70,14 @@ func (s *Suite) TestInvalidAction(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
||||||
// this ACL is wrong because the group in users sections doesn't exist
|
// this ACL is wrong because the group in Sources sections doesn't exist
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
Groups: Groups{
|
Groups: Groups{
|
||||||
"group:test": []string{"foo"},
|
"group:test": []string{"foo"},
|
||||||
"group:error": []string{"foo", "group:test"},
|
"group:error": []string{"foo", "group:test"},
|
||||||
},
|
},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"group:error"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -88,7 +88,7 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|||||||
// this ACL is wrong because no tagOwners own the requested tag for the server
|
// this ACL is wrong because no tagOwners own the requested tag for the server
|
||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"tag:foo"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := app.UpdateACLRules()
|
err := app.UpdateACLRules()
|
||||||
@@ -97,8 +97,8 @@ func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|||||||
|
|
||||||
// this test should validate that we can expand a group in a TagOWner section and
|
// this test should validate that we can expand a group in a TagOWner section and
|
||||||
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
||||||
// the tag is matched in the Users section.
|
// the tag is matched in the Sources section.
|
||||||
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
func (s *Suite) TestValidExpandTagOwnersInSources(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"tag:test"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -143,8 +143,8 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
|||||||
|
|
||||||
// this test should validate that we can expand a group in a TagOWner section and
|
// this test should validate that we can expand a group in a TagOWner section and
|
||||||
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
||||||
// the tag is matched in the Ports section.
|
// the tag is matched in the Destinations section.
|
||||||
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
|
func (s *Suite) TestValidExpandTagOwnersInDestinations(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("user1")
|
namespace, err := app.CreateNamespace("user1")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
|
|||||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
|
{Action: "accept", Sources: []string{"*"}, Destinations: []string{"tag:test:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -222,7 +222,7 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
|||||||
app.aclPolicy = &ACLPolicy{
|
app.aclPolicy = &ACLPolicy{
|
||||||
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"user1"}, Destinations: []string{"*:*"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = app.UpdateACLRules()
|
err = app.UpdateACLRules()
|
||||||
@@ -287,9 +287,9 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
|||||||
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
|
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{
|
{
|
||||||
Action: "accept",
|
Action: "accept",
|
||||||
Users: []string{"user1"},
|
Sources: []string{"user1"},
|
||||||
Ports: []string{"tag:webapp:80,443"},
|
Destinations: []string{"tag:webapp:80,443"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -321,6 +321,20 @@ func (s *Suite) TestPortRange(c *check.C) {
|
|||||||
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
|
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Suite) TestProtocolParsing(c *check.C) {
|
||||||
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_protocols.hujson")
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
rules, err := app.generateACLRules()
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
c.Assert(rules, check.NotNil)
|
||||||
|
|
||||||
|
c.Assert(rules, check.HasLen, 3)
|
||||||
|
c.Assert(rules[0].IPProto[0], check.Equals, protocolTCP)
|
||||||
|
c.Assert(rules[1].IPProto[0], check.Equals, protocolUDP)
|
||||||
|
c.Assert(rules[2].IPProto[1], check.Equals, protocolIPv6ICMP)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Suite) TestPortWildcard(c *check.C) {
|
func (s *Suite) TestPortWildcard(c *check.C) {
|
||||||
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_wildcards.hujson")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
@@ -628,7 +642,8 @@ func Test_expandTagOwners(t *testing.T) {
|
|||||||
|
|
||||||
func Test_expandPorts(t *testing.T) {
|
func Test_expandPorts(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
portsStr string
|
portsStr string
|
||||||
|
needsWildcard bool
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -638,15 +653,29 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "wildcard",
|
name: "wildcard",
|
||||||
args: args{portsStr: "*"},
|
args: args{portsStr: "*", needsWildcard: true},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "two ports",
|
name: "needs wildcard but does not require it",
|
||||||
args: args{portsStr: "80,443"},
|
args: args{portsStr: "*", needsWildcard: false},
|
||||||
|
want: &[]tailcfg.PortRange{
|
||||||
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "needs wildcard but gets port",
|
||||||
|
args: args{portsStr: "80,443", needsWildcard: true},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two Destinations",
|
||||||
|
args: args{portsStr: "80,443", needsWildcard: false},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: 80, Last: 80},
|
{First: 80, Last: 80},
|
||||||
{First: 443, Last: 443},
|
{First: 443, Last: 443},
|
||||||
@@ -655,7 +684,7 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "a range and a port",
|
name: "a range and a port",
|
||||||
args: args{portsStr: "80-1024,443"},
|
args: args{portsStr: "80-1024,443", needsWildcard: false},
|
||||||
want: &[]tailcfg.PortRange{
|
want: &[]tailcfg.PortRange{
|
||||||
{First: 80, Last: 1024},
|
{First: 80, Last: 1024},
|
||||||
{First: 443, Last: 443},
|
{First: 443, Last: 443},
|
||||||
@@ -664,38 +693,38 @@ func Test_expandPorts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "out of bounds",
|
name: "out of bounds",
|
||||||
args: args{portsStr: "854038"},
|
args: args{portsStr: "854038", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port",
|
name: "wrong port",
|
||||||
args: args{portsStr: "85a38"},
|
args: args{portsStr: "85a38", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port in first",
|
name: "wrong port in first",
|
||||||
args: args{portsStr: "a-80"},
|
args: args{portsStr: "a-80", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port in last",
|
name: "wrong port in last",
|
||||||
args: args{portsStr: "80-85a38"},
|
args: args{portsStr: "80-85a38", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong port format",
|
name: "wrong port format",
|
||||||
args: args{portsStr: "80-85a38-3"},
|
args: args{portsStr: "80-85a38-3", needsWildcard: false},
|
||||||
want: nil,
|
want: nil,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
got, err := expandPorts(test.args.portsStr)
|
got, err := expandPorts(test.args.portsStr, test.args.needsWildcard)
|
||||||
if (err != nil) != test.wantErr {
|
if (err != nil) != test.wantErr {
|
||||||
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
|
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
|
||||||
|
@@ -11,18 +11,19 @@ import (
|
|||||||
|
|
||||||
// ACLPolicy represents a Tailscale ACL Policy.
|
// ACLPolicy represents a Tailscale ACL Policy.
|
||||||
type ACLPolicy struct {
|
type ACLPolicy struct {
|
||||||
Groups Groups `json:"Groups" yaml:"Groups"`
|
Groups Groups `json:"groups" yaml:"groups"`
|
||||||
Hosts Hosts `json:"Hosts" yaml:"Hosts"`
|
Hosts Hosts `json:"hosts" yaml:"hosts"`
|
||||||
TagOwners TagOwners `json:"TagOwners" yaml:"TagOwners"`
|
TagOwners TagOwners `json:"tagOwners" yaml:"tagOwners"`
|
||||||
ACLs []ACL `json:"ACLs" yaml:"ACLs"`
|
ACLs []ACL `json:"acls" yaml:"acls"`
|
||||||
Tests []ACLTest `json:"Tests" yaml:"Tests"`
|
Tests []ACLTest `json:"tests" yaml:"tests"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACL is a basic rule for the ACL Policy.
|
// ACL is a basic rule for the ACL Policy.
|
||||||
type ACL struct {
|
type ACL struct {
|
||||||
Action string `json:"Action" yaml:"Action"`
|
Action string `json:"action" yaml:"action"`
|
||||||
Users []string `json:"Users" yaml:"Users"`
|
Protocol string `json:"proto" yaml:"proto"`
|
||||||
Ports []string `json:"Ports" yaml:"Ports"`
|
Sources []string `json:"src" yaml:"src"`
|
||||||
|
Destinations []string `json:"dst" yaml:"dst"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Groups references a series of alias in the ACL rules.
|
// Groups references a series of alias in the ACL rules.
|
||||||
@@ -36,9 +37,9 @@ type TagOwners map[string][]string
|
|||||||
|
|
||||||
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
// ACLTest is not implemented, but should be use to check if a certain rule is allowed.
|
||||||
type ACLTest struct {
|
type ACLTest struct {
|
||||||
User string `json:"User" yaml:"User"`
|
Source string `json:"src" yaml:"src"`
|
||||||
Allow []string `json:"Allow" yaml:"Allow"`
|
Accept []string `json:"accept" yaml:"accept"`
|
||||||
Deny []string `json:"Deny,omitempty" yaml:"Deny,omitempty"`
|
Deny []string `json:"deny,omitempty" yaml:"deny,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
// UnmarshalJSON allows to parse the Hosts directly into netaddr objects.
|
||||||
|
16
api_key.go
16
api_key.go
@@ -13,7 +13,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
apiPrefixLength = 7
|
apiPrefixLength = 7
|
||||||
apiKeyLength = 32
|
apiKeyLength = 32
|
||||||
apiKeyParts = 2
|
|
||||||
|
|
||||||
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
errAPIKeyFailedToParse = Error("Failed to parse ApiKey")
|
||||||
)
|
)
|
||||||
@@ -115,9 +114,9 @@ func (h *Headscale) ExpireAPIKey(key *APIKey) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
||||||
prefix, hash, err := splitAPIKey(keyStr)
|
prefix, hash, found := strings.Cut(keyStr, ".")
|
||||||
if err != nil {
|
if !found {
|
||||||
return false, fmt.Errorf("failed to validate api key: %w", err)
|
return false, errAPIKeyFailedToParse
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := h.GetAPIKey(prefix)
|
key, err := h.GetAPIKey(prefix)
|
||||||
@@ -136,15 +135,6 @@ func (h *Headscale) ValidateAPIKey(keyStr string) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitAPIKey(key string) (string, string, error) {
|
|
||||||
parts := strings.Split(key, ".")
|
|
||||||
if len(parts) != apiKeyParts {
|
|
||||||
return "", "", errAPIKeyFailedToParse
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[0], parts[1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (key *APIKey) toProto() *v1.ApiKey {
|
func (key *APIKey) toProto() *v1.ApiKey {
|
||||||
protoKey := v1.ApiKey{
|
protoKey := v1.ApiKey{
|
||||||
Id: key.ID,
|
Id: key.ID,
|
||||||
|
26
app.go
26
app.go
@@ -657,7 +657,9 @@ func (h *Headscale) Serve() error {
|
|||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("path", aclPath).
|
Str("path", aclPath).
|
||||||
Msg("ACL policy successfully reloaded")
|
Msg("ACL policy successfully reloaded, notifying nodes of change")
|
||||||
|
|
||||||
|
h.setLastStateChangeToNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -756,13 +758,25 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) setLastStateChangeToNow(namespace string) {
|
func (h *Headscale) setLastStateChangeToNow(namespaces ...string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
lastStateUpdate.WithLabelValues("", "headscale").Set(float64(now.Unix()))
|
|
||||||
if h.lastStateChange == nil {
|
if len(namespaces) == 0 {
|
||||||
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
namespaces, err = h.ListNamespacesStr()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Caller().Err(err).Msg("failed to fetch all namespaces, failing to update last changed state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, namespace := range namespaces {
|
||||||
|
lastStateUpdate.WithLabelValues(namespace, "headscale").Set(float64(now.Unix()))
|
||||||
|
if h.lastStateChange == nil {
|
||||||
|
h.lastStateChange = xsync.NewMapOf[time.Time]()
|
||||||
|
}
|
||||||
|
h.lastStateChange.Store(namespace, now)
|
||||||
}
|
}
|
||||||
h.lastStateChange.Store(namespace, now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
|
||||||
|
28
cmd/headscale/cli/dump_config.go
Normal file
28
cmd/headscale/cli/dump_config.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(dumpConfigCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dumpConfigCmd = &cobra.Command{
|
||||||
|
Use: "dumpConfig",
|
||||||
|
Short: "dump current config to /etc/headscale/config.dump.yaml, integration test only",
|
||||||
|
Hidden: true,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := viper.WriteConfigAs("/etc/headscale/config.dump.yaml")
|
||||||
|
if err != nil {
|
||||||
|
//nolint
|
||||||
|
fmt.Println("Failed to dump config")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
@@ -3,17 +3,75 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/juanfont/headscale"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tcnksm/go-latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cfgFile string = ""
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
cobra.OnInitialize(initConfig)
|
||||||
|
rootCmd.PersistentFlags().
|
||||||
|
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
|
||||||
rootCmd.PersistentFlags().
|
rootCmd.PersistentFlags().
|
||||||
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
|
StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'")
|
||||||
rootCmd.PersistentFlags().
|
rootCmd.PersistentFlags().
|
||||||
Bool("force", false, "Disable prompts and forces the execution")
|
Bool("force", false, "Disable prompts and forces the execution")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
if cfgFile != "" {
|
||||||
|
err := headscale.LoadConfig(cfgFile, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := headscale.LoadConfig("", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := headscale.GetHeadscaleConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Caller().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
machineOutput := HasMachineOutputFlag()
|
||||||
|
|
||||||
|
zerolog.SetGlobalLevel(cfg.LogLevel)
|
||||||
|
|
||||||
|
// If the user has requested a "machine" readable format,
|
||||||
|
// then disable login so the output remains valid.
|
||||||
|
if machineOutput {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.DisableUpdateCheck && !machineOutput {
|
||||||
|
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
||||||
|
Version != "dev" {
|
||||||
|
githubTag := &latest.GithubTag{
|
||||||
|
Owner: "juanfont",
|
||||||
|
Repository: "headscale",
|
||||||
|
}
|
||||||
|
res, err := latest.Check(githubTag, Version)
|
||||||
|
if err == nil && res.Outdated {
|
||||||
|
//nolint
|
||||||
|
fmt.Printf(
|
||||||
|
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
|
||||||
|
res.Current,
|
||||||
|
Version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "headscale",
|
Use: "headscale",
|
||||||
Short: "headscale - a Tailscale control server",
|
Short: "headscale - a Tailscale control server",
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/efekarakus/termcolor"
|
"github.com/efekarakus/termcolor"
|
||||||
"github.com/juanfont/headscale"
|
|
||||||
"github.com/juanfont/headscale/cmd/headscale/cli"
|
"github.com/juanfont/headscale/cmd/headscale/cli"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/tcnksm/go-latest"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -43,39 +39,5 @@ func main() {
|
|||||||
NoColor: !colors,
|
NoColor: !colors,
|
||||||
})
|
})
|
||||||
|
|
||||||
cfg, err := headscale.GetHeadscaleConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Caller().Err(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
machineOutput := cli.HasMachineOutputFlag()
|
|
||||||
|
|
||||||
zerolog.SetGlobalLevel(cfg.LogLevel)
|
|
||||||
|
|
||||||
// If the user has requested a "machine" readable format,
|
|
||||||
// then disable login so the output remains valid.
|
|
||||||
if machineOutput {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.DisableUpdateCheck && !machineOutput {
|
|
||||||
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
|
|
||||||
cli.Version != "dev" {
|
|
||||||
githubTag := &latest.GithubTag{
|
|
||||||
Owner: "juanfont",
|
|
||||||
Repository: "headscale",
|
|
||||||
}
|
|
||||||
res, err := latest.Check(githubTag, cli.Version)
|
|
||||||
if err == nil && res.Outdated {
|
|
||||||
//nolint
|
|
||||||
fmt.Printf(
|
|
||||||
"An updated version of Headscale has been found (%s vs. your current %s). Check it out https://github.com/juanfont/headscale/releases\n",
|
|
||||||
res.Current,
|
|
||||||
cli.Version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.Execute()
|
cli.Execute()
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,51 @@ func (s *Suite) SetUpSuite(c *check.C) {
|
|||||||
func (s *Suite) TearDownSuite(c *check.C) {
|
func (s *Suite) TearDownSuite(c *check.C) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Suite) TestConfigFileLoading(c *check.C) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
path, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile := filepath.Join(tmpDir, "config.yaml")
|
||||||
|
|
||||||
|
// Symlink the example config file
|
||||||
|
err = os.Symlink(
|
||||||
|
filepath.Clean(path+"/../../config-example.yaml"),
|
||||||
|
cfgFile,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load example config, it should load without validation errors
|
||||||
|
err = headscale.LoadConfig(cfgFile, true)
|
||||||
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
// Test that config file was interpreted correctly
|
||||||
|
c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080")
|
||||||
|
c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080")
|
||||||
|
c.Assert(viper.GetString("metrics_listen_addr"), check.Equals, "127.0.0.1:9090")
|
||||||
|
c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3")
|
||||||
|
c.Assert(viper.GetString("db_path"), check.Equals, "/var/lib/headscale/db.sqlite")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
||||||
|
c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01")
|
||||||
|
c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1")
|
||||||
|
c.Assert(
|
||||||
|
headscale.GetFileMode("unix_socket_permission"),
|
||||||
|
check.Equals,
|
||||||
|
fs.FileMode(0o770),
|
||||||
|
)
|
||||||
|
c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false)
|
||||||
|
}
|
||||||
|
|
||||||
func (*Suite) TestConfigLoading(c *check.C) {
|
func (*Suite) TestConfigLoading(c *check.C) {
|
||||||
tmpDir, err := ioutil.TempDir("", "headscale")
|
tmpDir, err := ioutil.TempDir("", "headscale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +94,7 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = headscale.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
// Test that config file was interpreted correctly
|
// Test that config file was interpreted correctly
|
||||||
@@ -92,7 +137,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load example config, it should load without validation errors
|
// Load example config, it should load without validation errors
|
||||||
err = headscale.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
dnsConfig, baseDomain := headscale.GetDNSConfig()
|
dnsConfig, baseDomain := headscale.GetDNSConfig()
|
||||||
@@ -125,7 +170,7 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
|||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
|
|
||||||
// Check configuration validation errors (1)
|
// Check configuration validation errors (1)
|
||||||
err = headscale.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
// check.Matches can not handle multiline strings
|
// check.Matches can not handle multiline strings
|
||||||
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
tmp := strings.ReplaceAll(err.Error(), "\n", "***")
|
||||||
@@ -150,6 +195,6 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
|
|||||||
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"",
|
||||||
)
|
)
|
||||||
writeConfig(c, tmpDir, configYaml)
|
writeConfig(c, tmpDir, configYaml)
|
||||||
err = headscale.LoadConfig(tmpDir)
|
err = headscale.LoadConfig(tmpDir, false)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
}
|
}
|
||||||
|
25
config.go
25
config.go
@@ -114,15 +114,19 @@ type ACLConfig struct {
|
|||||||
PolicyPath string
|
PolicyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) error {
|
func LoadConfig(path string, isFile bool) error {
|
||||||
viper.SetConfigName("config")
|
if isFile {
|
||||||
if path == "" {
|
viper.SetConfigFile(path)
|
||||||
viper.AddConfigPath("/etc/headscale/")
|
|
||||||
viper.AddConfigPath("$HOME/.headscale")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
} else {
|
} else {
|
||||||
// For testing
|
viper.SetConfigName("config")
|
||||||
viper.AddConfigPath(path)
|
if path == "" {
|
||||||
|
viper.AddConfigPath("/etc/headscale/")
|
||||||
|
viper.AddConfigPath("$HOME/.headscale")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
} else {
|
||||||
|
// For testing
|
||||||
|
viper.AddConfigPath(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetEnvPrefix("headscale")
|
viper.SetEnvPrefix("headscale")
|
||||||
@@ -377,11 +381,6 @@ func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetHeadscaleConfig() (*Config, error) {
|
func GetHeadscaleConfig() (*Config, error) {
|
||||||
err := LoadConfig("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsConfig, baseDomain := GetDNSConfig()
|
dnsConfig, baseDomain := GetDNSConfig()
|
||||||
derpConfig := GetDERPConfig()
|
derpConfig := GetDERPConfig()
|
||||||
logConfig := GetLogTailConfig()
|
logConfig := GetLogTailConfig()
|
||||||
|
@@ -27,6 +27,7 @@ written by community members. It is _not_ verified by `headscale` developers.
|
|||||||
**It might be outdated and it might miss necessary steps**.
|
**It might be outdated and it might miss necessary steps**.
|
||||||
|
|
||||||
- [Running headscale in a container](running-headscale-container.md)
|
- [Running headscale in a container](running-headscale-container.md)
|
||||||
|
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|
||||||
|
60
docs/acls.md
60
docs/acls.md
@@ -33,7 +33,7 @@ Note: Namespaces will be created automatically when users authenticate with the
|
|||||||
Headscale server.
|
Headscale server.
|
||||||
|
|
||||||
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
|
ACLs could be written either on [huJSON](https://github.com/tailscale/hujson)
|
||||||
or Yaml. Check the [test ACLs](../tests/acls) for further information.
|
or YAML. Check the [test ACLs](../tests/acls) for further information.
|
||||||
|
|
||||||
When registering the servers we will need to add the flag
|
When registering the servers we will need to add the flag
|
||||||
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||||
@@ -83,8 +83,8 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
// boss have access to all servers
|
// boss have access to all servers
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:boss"],
|
"src": ["group:boss"],
|
||||||
"ports": [
|
"dst": [
|
||||||
"tag:prod-databases:*",
|
"tag:prod-databases:*",
|
||||||
"tag:prod-app-servers:*",
|
"tag:prod-app-servers:*",
|
||||||
"tag:internal:*",
|
"tag:internal:*",
|
||||||
@@ -93,11 +93,12 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// admin have only access to administrative ports of the servers
|
// admin have only access to administrative ports of the servers, in tcp/22
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:admin"],
|
"src": ["group:admin"],
|
||||||
"ports": [
|
"proto": "tcp",
|
||||||
|
"dst": [
|
||||||
"tag:prod-databases:22",
|
"tag:prod-databases:22",
|
||||||
"tag:prod-app-servers:22",
|
"tag:prod-app-servers:22",
|
||||||
"tag:internal:22",
|
"tag:internal:22",
|
||||||
@@ -106,12 +107,26 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// we also allow admin to ping the servers
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"src": ["group:admin"],
|
||||||
|
"proto": "icmp",
|
||||||
|
"dst": [
|
||||||
|
"tag:prod-databases:*",
|
||||||
|
"tag:prod-app-servers:*",
|
||||||
|
"tag:internal:*",
|
||||||
|
"tag:dev-databases:*",
|
||||||
|
"tag:dev-app-servers:*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// developers have access to databases servers and application servers on all ports
|
// developers have access to databases servers and application servers on all ports
|
||||||
// they can only view the applications servers in prod and have no access to databases servers in production
|
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:dev"],
|
"src": ["group:dev"],
|
||||||
"ports": [
|
"dst": [
|
||||||
"tag:dev-databases:*",
|
"tag:dev-databases:*",
|
||||||
"tag:dev-app-servers:*",
|
"tag:dev-app-servers:*",
|
||||||
"tag:prod-app-servers:80,443"
|
"tag:prod-app-servers:80,443"
|
||||||
@@ -124,37 +139,38 @@ Here are the ACL's to implement the same permissions as above:
|
|||||||
// https://github.com/juanfont/headscale/issues/502
|
// https://github.com/juanfont/headscale/issues/502
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:dev"],
|
"src": ["group:dev"],
|
||||||
"ports": ["10.20.0.0/16:443,5432", "router.internal:0"]
|
"dst": ["10.20.0.0/16:443,5432", "router.internal:0"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// servers should be able to talk to database. Database should not be able to initiate connections to
|
// servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to
|
||||||
// applications servers
|
// applications servers
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["tag:dev-app-servers"],
|
"src": ["tag:dev-app-servers"],
|
||||||
"ports": ["tag:dev-databases:5432"]
|
"proto": "tcp",
|
||||||
|
"dst": ["tag:dev-databases:5432"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["tag:prod-app-servers"],
|
"src": ["tag:prod-app-servers"],
|
||||||
"ports": ["tag:prod-databases:5432"]
|
"dst": ["tag:prod-databases:5432"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// interns have access to dev-app-servers only in reading mode
|
// interns have access to dev-app-servers only in reading mode
|
||||||
{
|
{
|
||||||
"action": "accept",
|
"action": "accept",
|
||||||
"users": ["group:intern"],
|
"src": ["group:intern"],
|
||||||
"ports": ["tag:dev-app-servers:80,443"]
|
"dst": ["tag:dev-app-servers:80,443"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
||||||
// their own namespaces.
|
// their own namespaces.
|
||||||
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
{ "action": "accept", "src": ["boss"], "dst": ["boss:*"] },
|
||||||
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
{ "action": "accept", "src": ["dev1"], "dst": ["dev1:*"] },
|
||||||
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
{ "action": "accept", "src": ["dev2"], "dst": ["dev2:*"] },
|
||||||
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
{ "action": "accept", "src": ["admin1"], "dst": ["admin1:*"] },
|
||||||
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
{ "action": "accept", "src": ["intern1"], "dst": ["intern1:*"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
206
docs/running-headscale-openbsd.md
Normal file
206
docs/running-headscale-openbsd.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Running headscale on OpenBSD
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD 7.1.
|
||||||
|
In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd)
|
||||||
|
describing how to make `headscale` run properly in a server environment.
|
||||||
|
|
||||||
|
## Install `headscale`
|
||||||
|
|
||||||
|
1. Install from ports (Not Recommend)
|
||||||
|
|
||||||
|
As of OpenBSD 7.1, there's a headscale in ports collection, however, it's severely outdated(v0.12.4).
|
||||||
|
You can install it via `pkg_add headscale`.
|
||||||
|
|
||||||
|
2. Install from source on OpenBSD 7.1
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install prerequistes
|
||||||
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
pkg_add -D snap go
|
||||||
|
pkg_add gmake
|
||||||
|
|
||||||
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|
||||||
|
cd headscale
|
||||||
|
|
||||||
|
# optionally checkout a release
|
||||||
|
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
|
||||||
|
# option b. get latest tag, this may be a beta release
|
||||||
|
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
|
git checkout $latestTag
|
||||||
|
|
||||||
|
gmake build
|
||||||
|
|
||||||
|
# make it executable
|
||||||
|
chmod a+x headscale
|
||||||
|
|
||||||
|
# copy it to /usr/local/sbin
|
||||||
|
cp headscale /usr/local/sbin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install from source via cross compile
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install prerequistes
|
||||||
|
# 1. go v1.18+: headscale newer than 0.15 needs go 1.18+ to compile
|
||||||
|
# 2. gmake: Makefile in the headscale repo is written in GNU make syntax
|
||||||
|
|
||||||
|
git clone https://github.com/juanfont/headscale.git
|
||||||
|
|
||||||
|
cd headscale
|
||||||
|
|
||||||
|
# optionally checkout a release
|
||||||
|
# option a. you can find offical relase at https://github.com/juanfont/headscale/releases/latest
|
||||||
|
# option b. get latest tag, this may be a beta release
|
||||||
|
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
|
git checkout $latestTag
|
||||||
|
|
||||||
|
make build GOOS=openbsd
|
||||||
|
|
||||||
|
# copy headscale to openbsd machine and put it in /usr/local/sbin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure and run `headscale`
|
||||||
|
|
||||||
|
1. Prepare a directory to hold `headscale` configuration and the [SQLite](https://www.sqlite.org/) database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Directory for configuration
|
||||||
|
|
||||||
|
mkdir -p /etc/headscale
|
||||||
|
|
||||||
|
# Directory for Database, and other variable data (like certificates)
|
||||||
|
mkdir -p /var/lib/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create an empty SQLite database:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /var/lib/headscale/db.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a `headscale` configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
touch /etc/headscale/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml)
|
||||||
|
from the [headscale repository](../)
|
||||||
|
|
||||||
|
4. Start the headscale server:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale serve
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will start `headscale` in the current terminal session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
To continue the tutorial, open a new terminal and let it run in the background.
|
||||||
|
Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux).
|
||||||
|
|
||||||
|
To run `headscale` in the background, please follow the steps in the [rc.d section](#running-headscale-in-the-background-with-rcd) before continuing.
|
||||||
|
|
||||||
|
5. Verify `headscale` is running:
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Create a namespace ([tailnet](https://tailscale.com/kb/1136/tailnet/)):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale namespaces create myfirstnamespace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register a machine (normal login)
|
||||||
|
|
||||||
|
On a client machine, execute the `tailscale` login command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server YOUR_HEADSCALE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the machine:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --namespace myfirstnamespace nodes register --key <YOU_+MACHINE_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register machine using a pre authenticated key
|
||||||
|
|
||||||
|
Generate a key using the command line:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
headscale --namespace myfirstnamespace preauthkeys create --reusable --expiration 24h
|
||||||
|
```
|
||||||
|
|
||||||
|
This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running `headscale` in the background with rc.d
|
||||||
|
|
||||||
|
This section demonstrates how to run `headscale` as a service in the background with [rc.d](https://man.openbsd.org/rc.d).
|
||||||
|
|
||||||
|
1. Create a rc.d service at `/etc/rc.d/headscale` containing:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
#!/bin/ksh
|
||||||
|
|
||||||
|
daemon="/usr/local/sbin/headscale"
|
||||||
|
daemon_logger="daemon.info"
|
||||||
|
daemon_user="root"
|
||||||
|
daemon_flags="serve"
|
||||||
|
daemon_timeout=60
|
||||||
|
|
||||||
|
. /etc/rc.d/rc.subr
|
||||||
|
|
||||||
|
rc_bg=YES
|
||||||
|
rc_reload=NO
|
||||||
|
|
||||||
|
rc_cmd $1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `/etc/rc.d/headscale` needs execute permission:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod a+x /etc/rc.d/headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start `headscale` service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl start headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Make `headscale` service start at boot:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl enable headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Verify the headscale service:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
rcctl check headscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `headscale` is available:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl http://127.0.0.1:9090/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
`headscale` will now run in the background and start at boot.
|
@@ -1721,3 +1721,43 @@ func (s *IntegrationCLITestSuite) TestNodeMoveCommand() {
|
|||||||
|
|
||||||
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
assert.Equal(s.T(), machine.Namespace, oldNamespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationCLITestSuite) TestLoadConfigFromCommand() {
|
||||||
|
// TODO: make sure defaultConfig is not same as altConfig
|
||||||
|
defaultConfig, err := os.ReadFile("integration_test/etc/config.dump.gold.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
altConfig, err := os.ReadFile("integration_test/etc/alt-config.dump.gold.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
_, err = ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"dumpConfig",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
defaultDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.YAMLEq(s.T(), string(defaultConfig), string(defaultDumpConfig))
|
||||||
|
|
||||||
|
_, err = ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"-c",
|
||||||
|
"/etc/headscale/alt-config.yaml",
|
||||||
|
"dumpConfig",
|
||||||
|
},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
altDumpConfig, err := os.ReadFile("integration_test/etc/config.dump.yaml")
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
assert.YAMLEq(s.T(), string(altConfig), string(altDumpConfig))
|
||||||
|
}
|
||||||
|
46
integration_test/etc/alt-config.dump.gold.yaml
Normal file
46
integration_test/etc/alt-config.dump.gold.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
acl_policy_path: ""
|
||||||
|
cli:
|
||||||
|
insecure: false
|
||||||
|
timeout: 5s
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
db_type: sqlite3
|
||||||
|
derp:
|
||||||
|
auto_update_enabled: false
|
||||||
|
server:
|
||||||
|
enabled: false
|
||||||
|
stun:
|
||||||
|
enabled: true
|
||||||
|
update_frequency: 1m
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
domains: []
|
||||||
|
magic_dns: true
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
grpc_allow_insecure: false
|
||||||
|
grpc_listen_addr: :50443
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
listen_addr: 0.0.0.0:18080
|
||||||
|
log_level: disabled
|
||||||
|
logtail:
|
||||||
|
enabled: false
|
||||||
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
|
oidc:
|
||||||
|
scope:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
strip_email_domain: true
|
||||||
|
private_key_path: private.key
|
||||||
|
server_url: http://headscale:18080
|
||||||
|
tls_client_auth_mode: relaxed
|
||||||
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
tls_letsencrypt_challenge_type: HTTP-01
|
||||||
|
unix_socket: /var/run/headscale.sock
|
||||||
|
unix_socket_permission: "0o770"
|
||||||
|
|
24
integration_test/etc/alt-config.yaml
Normal file
24
integration_test/etc/alt-config.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
log_level: trace
|
||||||
|
acl_policy_path: ""
|
||||||
|
db_type: sqlite3
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
magic_dns: true
|
||||||
|
domains: []
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
private_key_path: private.key
|
||||||
|
listen_addr: 0.0.0.0:18080
|
||||||
|
metrics_listen_addr: 127.0.0.1:19090
|
||||||
|
server_url: http://headscale:18080
|
||||||
|
|
||||||
|
derp:
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
auto_update_enabled: false
|
||||||
|
update_frequency: 1m
|
46
integration_test/etc/config.dump.gold.yaml
Normal file
46
integration_test/etc/config.dump.gold.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
acl_policy_path: ""
|
||||||
|
cli:
|
||||||
|
insecure: false
|
||||||
|
timeout: 5s
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
db_type: sqlite3
|
||||||
|
derp:
|
||||||
|
auto_update_enabled: false
|
||||||
|
server:
|
||||||
|
enabled: false
|
||||||
|
stun:
|
||||||
|
enabled: true
|
||||||
|
update_frequency: 1m
|
||||||
|
urls:
|
||||||
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
domains: []
|
||||||
|
magic_dns: true
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
grpc_allow_insecure: false
|
||||||
|
grpc_listen_addr: :50443
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
listen_addr: 0.0.0.0:8080
|
||||||
|
log_level: disabled
|
||||||
|
logtail:
|
||||||
|
enabled: false
|
||||||
|
metrics_listen_addr: 127.0.0.1:9090
|
||||||
|
oidc:
|
||||||
|
scope:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
strip_email_domain: true
|
||||||
|
private_key_path: private.key
|
||||||
|
server_url: http://headscale:8080
|
||||||
|
tls_client_auth_mode: relaxed
|
||||||
|
tls_letsencrypt_cache_dir: /var/www/.cache
|
||||||
|
tls_letsencrypt_challenge_type: HTTP-01
|
||||||
|
unix_socket: /var/run/headscale.sock
|
||||||
|
unix_socket_permission: "0o770"
|
||||||
|
|
@@ -188,8 +188,8 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
|||||||
Hosts: map[string]netaddr.IPPrefix{},
|
Hosts: map[string]netaddr.IPPrefix{},
|
||||||
TagOwners: map[string][]string{},
|
TagOwners: map[string][]string{},
|
||||||
ACLs: []ACL{
|
ACLs: []ACL{
|
||||||
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
|
{Action: "accept", Sources: []string{"admin"}, Destinations: []string{"*:*"}},
|
||||||
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
|
{Action: "accept", Sources: []string{"test"}, Destinations: []string{"test:*"}},
|
||||||
},
|
},
|
||||||
Tests: []ACLTest{},
|
Tests: []ACLTest{},
|
||||||
}
|
}
|
||||||
|
@@ -148,6 +148,21 @@ func (h *Headscale) ListNamespaces() ([]Namespace, error) {
|
|||||||
return namespaces, nil
|
return namespaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) ListNamespacesStr() ([]string, error) {
|
||||||
|
namespaces, err := h.ListNamespaces()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceStrs := make([]string, len(namespaces))
|
||||||
|
|
||||||
|
for index, namespace := range namespaces {
|
||||||
|
namespaceStrs[index] = namespace.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespaceStrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
// ListMachinesInNamespace gets all the nodes in a given namespace.
|
||||||
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
func (h *Headscale) ListMachinesInNamespace(name string) ([]Machine, error) {
|
||||||
err := CheckForFQDNRules(name)
|
err := CheckForFQDNRules(name)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
// Declare static groups of users beyond those in the identity service.
|
// Declare static groups of users beyond those in the identity service.
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
"user2@example.com",
|
"user2@example.com",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Declare hostname aliases to use in place of IP addresses or subnets.
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"example-host-1": "100.100.100.100",
|
"example-host-1": "100.100.100.100",
|
||||||
"example-host-2": "100.100.101.100/24",
|
"example-host-2": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
// Define who is allowed to use which tags.
|
// Define who is allowed to use which tags.
|
||||||
"TagOwners": {
|
"tagOwners": {
|
||||||
// Everyone in the montreal-admins or global-admins group are
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
// allowed to tag servers as montreal-webserver.
|
// allowed to tag servers as montreal-webserver.
|
||||||
"tag:montreal-webserver": [
|
"tag:montreal-webserver": [
|
||||||
@@ -29,17 +29,17 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Access control lists.
|
// Access control lists.
|
||||||
"ACLs": [
|
"acls": [
|
||||||
// Engineering users, plus the president, can access port 22 (ssh)
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
// and port 3389 (remote desktop protocol) on all servers, and all
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
// ports on git-server or ci-server.
|
// ports on git-server or ci-server.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example2",
|
"group:example2",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"git-server:*",
|
"git-server:*",
|
||||||
"ci-server:*"
|
"ci-server:*"
|
||||||
@@ -48,22 +48,22 @@
|
|||||||
// Allow engineer users to access any port on a device tagged with
|
// Allow engineer users to access any port on a device tagged with
|
||||||
// tag:production.
|
// tag:production.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example"
|
"group:example"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:production:*"
|
"tag:production:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
// on both networks.
|
// on both networks.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"example-host-2",
|
"example-host-2",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"example-host-1:*",
|
"example-host-1:*",
|
||||||
"192.168.1.0/24:*"
|
"192.168.1.0/24:*"
|
||||||
],
|
],
|
||||||
@@ -72,22 +72,22 @@
|
|||||||
// Comment out this section if you want to define specific ACL
|
// Comment out this section if you want to define specific ACL
|
||||||
// restrictions above.
|
// restrictions above.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:*"
|
"*:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// All users in Montreal are allowed to access the Montreal web
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
// servers.
|
// servers.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"example-host-1"
|
"example-host-1"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:montreal-webserver:80,443"
|
"tag:montreal-webserver:80,443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -96,30 +96,30 @@
|
|||||||
// In contrast, this doesn't grant API servers the right to initiate
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
// any connections.
|
// any connections.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"tag:montreal-webserver"
|
"tag:montreal-webserver"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:api-server:443"
|
"tag:api-server:443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Declare tests to check functionality of ACL rules
|
// Declare tests to check functionality of ACL rules
|
||||||
"Tests": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"User": "user1@example.com",
|
"src": "user1@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"example-host-1:22",
|
"example-host-1:22",
|
||||||
"example-host-2:80"
|
"example-host-2:80"
|
||||||
],
|
],
|
||||||
"Deny": [
|
"deny": [
|
||||||
"exapmle-host-2:100"
|
"exapmle-host-2:100"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"User": "user2@example.com",
|
"src": "user2@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"100.60.3.4:22"
|
"100.60.3.4:22"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -3,19 +3,19 @@
|
|||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"subnet-1",
|
"subnet-1",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
// This ACL is used to test group expansion
|
// This ACL is used to test group expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"testnamespace",
|
"testnamespace",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:example",
|
"group:example",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test namespace expansion
|
// This ACL is used to test namespace expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"testnamespace",
|
"testnamespace",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
41
tests/acls/acl_policy_basic_protocols.hujson
Normal file
41
tests/acls/acl_policy_basic_protocols.hujson
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// This ACL is used to test wildcards
|
||||||
|
|
||||||
|
{
|
||||||
|
"hosts": {
|
||||||
|
"host-1": "100.100.100.100",
|
||||||
|
"subnet-1": "100.100.101.100/24",
|
||||||
|
},
|
||||||
|
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "tcp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "udp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:53",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Action": "accept",
|
||||||
|
"src": [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
"proto": "icmp",
|
||||||
|
"dst": [
|
||||||
|
"host-1:*",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test the port range expansion
|
// This ACL is used to test the port range expansion
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"subnet-1",
|
"subnet-1",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:5400-5500",
|
"host-1:5400-5500",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
// This ACL is used to test wildcards
|
// This ACL is used to test wildcards
|
||||||
|
|
||||||
{
|
{
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"host-1": "100.100.100.100",
|
"host-1": "100.100.100.100",
|
||||||
"subnet-1": "100.100.101.100/24",
|
"subnet-1": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
|
|
||||||
"ACLs": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"Action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*",
|
"*",
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"host-1:*",
|
"host-1:*",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
Hosts:
|
hosts:
|
||||||
host-1: 100.100.100.100/32
|
host-1: 100.100.100.100/32
|
||||||
subnet-1: 100.100.101.100/24
|
subnet-1: 100.100.101.100/24
|
||||||
ACLs:
|
acls:
|
||||||
- Action: accept
|
- action: accept
|
||||||
Users:
|
src:
|
||||||
- "*"
|
- "*"
|
||||||
Ports:
|
dst:
|
||||||
- host-1:*
|
- host-1:*
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
// Declare static groups of users beyond those in the identity service.
|
// Declare static groups of users beyond those in the identity service.
|
||||||
"Groups": {
|
"groups": {
|
||||||
"group:example": [
|
"group:example": [
|
||||||
"user1@example.com",
|
"user1@example.com",
|
||||||
"user2@example.com",
|
"user2@example.com",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Declare hostname aliases to use in place of IP addresses or subnets.
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
||||||
"Hosts": {
|
"hosts": {
|
||||||
"example-host-1": "100.100.100.100",
|
"example-host-1": "100.100.100.100",
|
||||||
"example-host-2": "100.100.101.100/24",
|
"example-host-2": "100.100.101.100/24",
|
||||||
},
|
},
|
||||||
// Define who is allowed to use which tags.
|
// Define who is allowed to use which tags.
|
||||||
"TagOwners": {
|
"tagOwners": {
|
||||||
// Everyone in the montreal-admins or global-admins group are
|
// Everyone in the montreal-admins or global-admins group are
|
||||||
// allowed to tag servers as montreal-webserver.
|
// allowed to tag servers as montreal-webserver.
|
||||||
"tag:montreal-webserver": [
|
"tag:montreal-webserver": [
|
||||||
@@ -26,17 +26,17 @@
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Access control lists.
|
// Access control lists.
|
||||||
"ACLs": [
|
"acls": [
|
||||||
// Engineering users, plus the president, can access port 22 (ssh)
|
// Engineering users, plus the president, can access port 22 (ssh)
|
||||||
// and port 3389 (remote desktop protocol) on all servers, and all
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
||||||
// ports on git-server or ci-server.
|
// ports on git-server or ci-server.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:engineering",
|
"group:engineering",
|
||||||
"president@example.com"
|
"president@example.com"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:22,3389",
|
"*:22,3389",
|
||||||
"git-server:*",
|
"git-server:*",
|
||||||
"ci-server:*"
|
"ci-server:*"
|
||||||
@@ -45,23 +45,23 @@
|
|||||||
// Allow engineer users to access any port on a device tagged with
|
// Allow engineer users to access any port on a device tagged with
|
||||||
// tag:production.
|
// tag:production.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:engineers"
|
"group:engineers"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:production:*"
|
"tag:production:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
||||||
// on both networks.
|
// on both networks.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"my-subnet",
|
"my-subnet",
|
||||||
"192.168.1.0/24"
|
"192.168.1.0/24"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"my-subnet:*",
|
"my-subnet:*",
|
||||||
"192.168.1.0/24:*"
|
"192.168.1.0/24:*"
|
||||||
],
|
],
|
||||||
@@ -70,22 +70,22 @@
|
|||||||
// Comment out this section if you want to define specific ACL
|
// Comment out this section if you want to define specific ACL
|
||||||
// restrictions above.
|
// restrictions above.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"*:*"
|
"*:*"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// All users in Montreal are allowed to access the Montreal web
|
// All users in Montreal are allowed to access the Montreal web
|
||||||
// servers.
|
// servers.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"group:montreal-users"
|
"group:montreal-users"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:montreal-webserver:80,443"
|
"tag:montreal-webserver:80,443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -94,30 +94,30 @@
|
|||||||
// In contrast, this doesn't grant API servers the right to initiate
|
// In contrast, this doesn't grant API servers the right to initiate
|
||||||
// any connections.
|
// any connections.
|
||||||
{
|
{
|
||||||
"Action": "accept",
|
"action": "accept",
|
||||||
"Users": [
|
"src": [
|
||||||
"tag:montreal-webserver"
|
"tag:montreal-webserver"
|
||||||
],
|
],
|
||||||
"Ports": [
|
"dst": [
|
||||||
"tag:api-server:443"
|
"tag:api-server:443"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Declare tests to check functionality of ACL rules
|
// Declare tests to check functionality of ACL rules
|
||||||
"Tests": [
|
"tests": [
|
||||||
{
|
{
|
||||||
"User": "user1@example.com",
|
"src": "user1@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"example-host-1:22",
|
"example-host-1:22",
|
||||||
"example-host-2:80"
|
"example-host-2:80"
|
||||||
],
|
],
|
||||||
"Deny": [
|
"deny": [
|
||||||
"exapmle-host-2:100"
|
"exapmle-host-2:100"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"User": "user2@example.com",
|
"src": "user2@example.com",
|
||||||
"Allow": [
|
"accept": [
|
||||||
"100.60.3.4:22"
|
"100.60.3.4:22"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user