mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-31 04:41:28 +02:00
Compare commits
3 Commits
fix-setcon
...
fix/empty-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc13d9fe7 | ||
|
|
c1f0006012 | ||
|
|
cff49237b6 |
72
SECURITY.md
72
SECURITY.md
@@ -1,12 +1,70 @@
|
||||
# Security Policy
|
||||
|
||||
NetBird's goal is to provide a secure network. If you find a vulnerability or bug, please report it by opening an issue [here](https://github.com/netbirdio/netbird/issues/new?assignees=&labels=&template=bug-issue-report.md&title=) or by contacting us by email.
|
||||
|
||||
There has yet to be an official bug bounty program for the NetBird project.
|
||||
|
||||
## Supported Versions
|
||||
- We currently support only the latest version
|
||||
NetBird's goal is to provide a secure network. The client runs as a privileged service on every machine it is installed on,
|
||||
so we take reports about it seriously and we publish what we fix.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `security@netbird.io`
|
||||
**Please do not open a public issue for a security vulnerability.** Public issues are visible to everyone, including before
|
||||
a fix is available.
|
||||
|
||||
Report security issues one of these two ways:
|
||||
|
||||
- **GitHub private vulnerability reporting** — [open a private report](https://github.com/netbirdio/netbird/security/advisories/new)
|
||||
on this repository. This is the preferred route: it keeps the discussion, the draft advisory, and the credit in one place.
|
||||
- **Email** — `security@netbird.io`.
|
||||
|
||||
If the finding affects NetBird Cloud or our hosted infrastructure rather than the open-source code, email us rather than
|
||||
filing a repository report.
|
||||
|
||||
### What to include
|
||||
|
||||
A report is easier to act on when it contains:
|
||||
|
||||
- The affected component (client, management, signal, relay, dashboard) and the version or commit you tested
|
||||
- The platform and configuration, where relevant — operating system, self-hosted or NetBird Cloud, container or host install
|
||||
- What an attacker needs before they can exploit it: network position, an account, local access, a specific privilege level
|
||||
- Steps to reproduce, and a proof of concept if you have one
|
||||
- The impact you believe it has
|
||||
|
||||
Partial reports are still welcome. If you are unsure whether something is a security issue, send it to `security@netbird.io`
|
||||
and let us make that call.
|
||||
|
||||
## What to expect from us
|
||||
|
||||
- **We acknowledge your report** and tell you whether we can reproduce it.
|
||||
- **We work with you on severity and scope.** If we assess it differently than you do, we will explain why rather than
|
||||
silently downgrade it.
|
||||
- **We fix and release**, then publish a [GitHub Security Advisory](https://github.com/netbirdio/netbird/security/advisories)
|
||||
naming the affected version range and the patched version.
|
||||
- **We credit reporters who want to be credited.** Tell us the name or handle you would like used, or that you would rather
|
||||
stay anonymous.
|
||||
- **We keep you in the loop** until the advisory is published.
|
||||
|
||||
We ask that you give us a reasonable opportunity to ship a fix before disclosing the issue publicly, and that you avoid
|
||||
accessing, modifying, or exfiltrating data belonging to other people while testing. Testing against your own installation
|
||||
or your own account is always fine.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We support the latest release. Security fixes ship in the next version rather than as backports to older releases, so
|
||||
upgrading to the current release is how you get them.
|
||||
|
||||
Release notifications are available by watching [releases](https://github.com/netbirdio/netbird/releases).
|
||||
|
||||
## Published advisories
|
||||
|
||||
Every vulnerability we fix is published as a GitHub Security Advisory on the
|
||||
[advisories page](https://github.com/netbirdio/netbird/security/advisories), including the affected version range, the
|
||||
patched version, and the reporter's credit. Advisories for the Go module are also distributed through the Go vulnerability
|
||||
database, so `govulncheck` will report them against your dependencies.
|
||||
|
||||
## Bug bounty
|
||||
|
||||
There is no official bug bounty program for the NetBird project. We credit reporters in advisories, and we are grateful for
|
||||
the work, but we cannot currently offer payment for reports.
|
||||
|
||||
## Non-security bugs
|
||||
|
||||
For bugs that are not security issues, please use the
|
||||
[issue tracker](https://github.com/netbirdio/netbird/discussions/new/choose).
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// The daemon takes guardedConfigMu before s.mutex. authorizeAndPrepareLogin
|
||||
// takes s.mutex while holding guardedConfigMu, so a SetConfig that grabbed
|
||||
// s.mutex first and then waited for guardedConfigMu would deadlock the daemon
|
||||
// against a concurrent login: two unprivileged IPC calls are enough.
|
||||
//
|
||||
// The held guardedConfigMu below stands in for that login. While SetConfig waits
|
||||
// for it, s.mutex must stay free, otherwise the login waiting for s.mutex could
|
||||
// never release guardedConfigMu.
|
||||
func TestSetConfig_TakesGuardedConfigMuBeforeServerMutex(t *testing.T) {
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
s.guardedConfigMu.Lock()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
Username: username,
|
||||
})
|
||||
done <- err
|
||||
}()
|
||||
|
||||
require.Never(t, func() bool {
|
||||
if !s.mutex.TryLock() {
|
||||
return true
|
||||
}
|
||||
s.mutex.Unlock()
|
||||
return false
|
||||
}, 500*time.Millisecond, 10*time.Millisecond,
|
||||
"SetConfig held s.mutex while waiting for guardedConfigMu, which deadlocks against a concurrent login")
|
||||
|
||||
s.guardedConfigMu.Unlock()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("SetConfig did not finish after guardedConfigMu was released")
|
||||
}
|
||||
}
|
||||
@@ -393,16 +393,6 @@ func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (i
|
||||
|
||||
// Login uses setup key to prepare configuration for the daemon.
|
||||
func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigRequest) (*proto.SetConfigResponse, error) {
|
||||
// Privilege gate: refuse the parts of the request that would let a local
|
||||
// user turn the root daemon into a root shell. Held across the write so the
|
||||
// config cannot gain the SSH server between the decision and the update.
|
||||
//
|
||||
// Taken before s.mutex: authorizeAndPrepareLogin takes s.mutex while holding
|
||||
// guardedConfigMu, so acquiring the two in the other order here would let a
|
||||
// concurrent login deadlock the daemon.
|
||||
s.guardedConfigMu.Lock()
|
||||
defer s.guardedConfigMu.Unlock()
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
@@ -427,6 +417,12 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Privilege gate: refuse the parts of the request that would let a local
|
||||
// user turn the root daemon into a root shell. Held across the write so the
|
||||
// config cannot gain the SSH server between the decision and the update.
|
||||
s.guardedConfigMu.Lock()
|
||||
defer s.guardedConfigMu.Unlock()
|
||||
|
||||
stored, err := s.storedProfileConfig(msg.ProfileName, msg.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -29,8 +29,13 @@ cask "{{ $projectName }}" do
|
||||
end
|
||||
|
||||
uninstall_preflight do
|
||||
system_command "#{appdir}/Netbird UI.app/uninstaller.sh",
|
||||
sudo: false
|
||||
system_command "/bin/sh",
|
||||
args: ["-c", <<~CMD],
|
||||
launchctl bootout system/netbird 2>/dev/null || \
|
||||
launchctl unload /Library/LaunchDaemons/netbird.plist 2>/dev/null || true
|
||||
rm -f /Library/LaunchDaemons/netbird.plist
|
||||
CMD
|
||||
sudo: true
|
||||
end
|
||||
|
||||
name "Netbird UI"
|
||||
|
||||
@@ -61,6 +61,8 @@ func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvel
|
||||
return &proto.NetworkMapEnvelope{
|
||||
Payload: &proto.NetworkMapEnvelope_Full{
|
||||
Full: &proto.NetworkMapComponentsFull{
|
||||
Serial: networkSerial(c.Network),
|
||||
Network: toAccountNetwork(c.Network),
|
||||
PeerConfig: in.PeerConfig,
|
||||
// components.Peers always contains the target peer
|
||||
Peers: []*proto.PeerCompact{toPeerCompact(c.Peers[c.PeerID])},
|
||||
|
||||
@@ -758,6 +758,9 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) {
|
||||
assert.Equal(t, "netbird.cloud", full.DnsDomain)
|
||||
assert.Len(t, full.Peers, 1)
|
||||
assert.Empty(t, full.Policies)
|
||||
require.NotNil(t, full.Network, "client runs Calculate() over the envelope and dereferences Network unconditionally; a nil here would crash the receiver")
|
||||
assert.Equal(t, "net-empty", full.Network.Identifier)
|
||||
assert.Equal(t, uint64(9), full.Serial)
|
||||
}
|
||||
|
||||
func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) {
|
||||
@@ -776,6 +779,12 @@ func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) {
|
||||
func emptyNetworkMapComponents() *types.NetworkMapComponents {
|
||||
return types.EmptyNetworkMapComponents(
|
||||
&types.NetworkMapComponents{
|
||||
PeerID: "peer-id", Peers: map[string]*types.ComponentPeer{"peer-id": {}}},
|
||||
PeerID: "peer-id", Peers: map[string]*types.ComponentPeer{"peer-id": {}},
|
||||
Network: &types.Network{
|
||||
Identifier: "net-empty",
|
||||
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||
Serial: 9,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,15 +228,17 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// decodeAccountNetwork never returns nil — Calculate() dereferences
|
||||
// c.Network unconditionally, and servers that predate the fix omit the field
|
||||
// entirely from the empty-components envelope.
|
||||
func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network {
|
||||
n := &types.Network{}
|
||||
if an == nil {
|
||||
return nil
|
||||
}
|
||||
n := &types.Network{
|
||||
Identifier: an.Identifier,
|
||||
Dns: an.Dns,
|
||||
Serial: an.Serial,
|
||||
return n
|
||||
}
|
||||
n.Identifier = an.Identifier
|
||||
n.Dns = an.Dns
|
||||
n.Serial = an.Serial
|
||||
if an.NetCidr != "" {
|
||||
if _, ipnet, err := net.ParseCIDR(an.NetCidr); err == nil && ipnet != nil {
|
||||
n.Net = *ipnet
|
||||
|
||||
@@ -221,6 +221,66 @@ func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) {
|
||||
"client-side Calculate must connect the same remote peers as the server")
|
||||
}
|
||||
|
||||
// TestEnvelopeToNetworkMap_EmptyComponents covers the graceful-degrade path
|
||||
// the server takes for a peer that is missing from the account or absent from
|
||||
// the validated-peers map. The legacy server short-circuited before
|
||||
// Calculate() and shipped a NetworkMap carrying only the account Network; the
|
||||
// components path runs Calculate() on the client instead, so the envelope must
|
||||
// carry Network or the client panics dereferencing a nil *types.Network.
|
||||
func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) {
|
||||
localPeerKey := randomWgKey(t)
|
||||
c := types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
|
||||
PeerID: "peer-A",
|
||||
Network: &types.Network{
|
||||
Identifier: "net-empty",
|
||||
Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)},
|
||||
Serial: 7,
|
||||
},
|
||||
Peers: map[string]*types.ComponentPeer{
|
||||
"peer-A": {ID: "peer-A", Key: localPeerKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1})},
|
||||
},
|
||||
})
|
||||
|
||||
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||
Components: c,
|
||||
DNSDomain: "netbird.cloud",
|
||||
})
|
||||
require.NotNil(t, envelope.GetFull().Network, "empty envelope must carry the account Network")
|
||||
|
||||
wire, err := goproto.Marshal(envelope)
|
||||
require.NoError(t, err, "marshal envelope")
|
||||
var decoded proto.NetworkMapEnvelope
|
||||
require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope")
|
||||
|
||||
result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud")
|
||||
require.NoError(t, err, "EnvelopeToNetworkMap must degrade gracefully on empty components")
|
||||
require.Equal(t, uint64(7), result.NetworkMap.Serial)
|
||||
require.Empty(t, result.NetworkMap.RemotePeers, "unvalidated peer connects to nobody")
|
||||
}
|
||||
|
||||
// TestEnvelopeToNetworkMap_MissingNetwork simulates a server that omits
|
||||
// AccountNetwork from the envelope. Clients must degrade rather than panic, so
|
||||
// they survive talking to a management server that predates the encoder fix.
|
||||
func TestEnvelopeToNetworkMap_MissingNetwork(t *testing.T) {
|
||||
c, localPeerKey := buildSmokeComponents(t)
|
||||
|
||||
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{
|
||||
Components: c,
|
||||
DNSDomain: "netbird.cloud",
|
||||
})
|
||||
envelope.GetFull().Network = nil
|
||||
|
||||
wire, err := goproto.Marshal(envelope)
|
||||
require.NoError(t, err, "marshal envelope")
|
||||
var decoded proto.NetworkMapEnvelope
|
||||
require.NoError(t, goproto.Unmarshal(wire, &decoded), "unmarshal envelope")
|
||||
|
||||
result, err := nbnetworkmap.EnvelopeToNetworkMap(context.Background(), &decoded, localPeerKey, "netbird.cloud")
|
||||
require.NoError(t, err, "a missing AccountNetwork must not panic the client")
|
||||
require.NotNil(t, result.Components.Network)
|
||||
require.NotEmpty(t, result.NetworkMap.RemotePeers, "the rest of the snapshot stays usable")
|
||||
}
|
||||
|
||||
// buildSmokeComponents returns a minimal NetworkMapComponents (2 peers, 1
|
||||
// group, 1 allow policy) plus the receiving peer's WG public key. Sufficient
|
||||
// to validate the encode → marshal → decode → Calculate pipeline produces
|
||||
|
||||
Reference in New Issue
Block a user