mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-31 02:34:07 +09:00
policy: pin SSH forwarding behavior and recommend upgrades
This commit is contained in:
1 parent
bd1876e1fa
commit
998c4b0619
11 files changed
+15977
-1
No files matched your search
+1
-1
@@ -29,7 +29,7 @@ config*.yaml
|
||||
!config-example.yaml
|
||||
derp.yaml
|
||||
*.hujson
|
||||
!hscontrol/policy/v2/testdata/*/*.hujson
|
||||
!hscontrol/policy/v2/testdata/**/*.hujson
|
||||
*.key
|
||||
/db.sqlite
|
||||
*.sqlite3
|
||||
|
||||
@@ -150,6 +150,22 @@ the ACME DNS-01 challenge through a built-in DNS provider: `acmedns`,
|
||||
- Deleting an ephemeral node no longer logs a spurious `ephemeral node deletion failed` error later [#3575](https://github.com/juanfont/headscale/pull/3575)
|
||||
- Add the v2 `acl/validate` endpoint, which Terraform provider 0.29.3 and newer call to check `tailscale_acl` at plan time; before it, those releases failed with `invalid character 'p' after top-level value`
|
||||
|
||||
## 0.29.7 (202x-xx-xx)
|
||||
|
||||
### Security
|
||||
|
||||
We recommend that all users upgrade to Headscale 0.29.7 and Tailscale v1.104.4 or newer,
|
||||
especially those using Tailscale SSH. Tailscale v1.104.4 fixes vulnerabilities in SSH
|
||||
forwarding, SSH session handling and Serve. These fixes run in `tailscaled` on your
|
||||
nodes; upgrading Headscale alone does not apply them. See Tailscale's
|
||||
[security bulletins](https://tailscale.com/security-bulletins#ts-2026-014) and
|
||||
[Tailscale SSH: Findings and fixes](https://tailscale.com/blog/tailscale-ssh-findings-fixes).
|
||||
|
||||
Headscale follows Tailscale's SSH policy behavior: forwarding is allowed by default,
|
||||
with no per-rule forwarding controls. As an interim mitigation for SSH forwarding
|
||||
vulnerabilities, set `TS_SSH_DISABLE_FORWARDING=1` in the destination `tailscaled`
|
||||
service environment and restart the service, as described in the security bulletin.
|
||||
|
||||
## 0.29.6 (2026-10-24)
|
||||
|
||||
**Minimum supported Tailscale client version: v1.80.0**
|
||||
|
||||
@@ -58,6 +58,68 @@ func loadSSHTestFile(t *testing.T, path string) *testcapture.Capture {
|
||||
return c
|
||||
}
|
||||
|
||||
// TestSSHForwardingDataCompat pins upstream forwarding actions and rejects
|
||||
// unsupported policy fields. The complete captures retain SSHUsers, but its new
|
||||
// numeric-user deny entry needs separate user-mapping compatibility coverage.
|
||||
func TestSSHForwardingDataCompat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files, err := filepath.Glob(filepath.Join("testdata", "ssh_results", "forwarding", "ssh-*.hujson"))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, files)
|
||||
|
||||
users := setupSSHDataCompatUsers()
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadSSHTestFile(t, file)
|
||||
|
||||
t.Run(tf.TestID, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Empty(t, tf.CaptureError)
|
||||
require.NotEmpty(t, tf.Captures)
|
||||
|
||||
pol, err := unmarshalPolicy([]byte(tf.Input.FullPolicy))
|
||||
if tf.Input.APIResponseCode != 200 {
|
||||
require.Equal(t, 400, tf.Input.APIResponseCode)
|
||||
require.NotNil(t, tf.Input.APIResponseBody)
|
||||
field, ok := strings.CutPrefix(tf.Input.APIResponseBody.Message, "json: unknown field ")
|
||||
require.True(t, ok, "expected an upstream unknown-field rejection")
|
||||
require.ErrorIs(t, err, ErrUnknownField)
|
||||
require.ErrorContains(t, err, fmt.Sprintf("%s: %s", ErrUnknownField, field))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
nodes := buildGrantsNodesFromCapture(users, tf)
|
||||
|
||||
for nodeName, capture := range tf.Captures {
|
||||
t.Run(nodeName, func(t *testing.T) {
|
||||
node := findNodeByGivenName(nodes, nodeName)
|
||||
require.NotNil(t, node)
|
||||
|
||||
got, err := pol.compileSSHPolicy("https://unused", users, node.View(), nodes.ViewSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
var rules []*tailcfg.SSHRule
|
||||
if got != nil {
|
||||
rules = got.Rules
|
||||
}
|
||||
|
||||
require.Len(t, rules, len(capture.SSHRules))
|
||||
|
||||
for i, want := range capture.SSHRules {
|
||||
require.NotNil(t, want.Action)
|
||||
assert.Equal(t, want.Action, rules[i].Action)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// sshSkipReasons documents captures the upstream control plane accepts
|
||||
// but headscale cannot yet represent. Each entry names the feature gap.
|
||||
var sshSkipReasons = map[string]string{
|
||||
|
||||
+1977
File diff suppressed because it is too large.
Load diff
+2001
File diff suppressed because it is too large.
Load diff
+1985
File diff suppressed because it is too large.
Load diff
+1985
File diff suppressed because it is too large.
Load diff
+1985
File diff suppressed because it is too large.
Load diff
+1995
File diff suppressed because it is too large.
Load diff
+1985
File diff suppressed because it is too large.
Load diff
+1985
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user