Skip to content

Commit 260332c

Browse files
authored
Add use_forwarded_for_headers configuration option for LAPI (crowdsecurity#610)
* Add use_forwarded_for_headers configuration option for LAPI * update documentation
1 parent 9f515cb commit 260332c

File tree

6 files changed

+179
-13
lines changed

6 files changed

+179
-13
lines changed

docs/v1.X/docs/references/crowdsec-config.md

+9
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ api:
5050
log_level: info
5151
listen_uri: 127.0.0.1:8080
5252
profiles_path: /etc/crowdsec/profiles.yaml
53+
use_forwarded_for_headers: false
5354
online_client: # Crowdsec API
5455
credentials_path: /etc/crowdsec/online_api_credentials.yaml
5556
# tls:
@@ -132,6 +133,7 @@ api:
132133
log_level: (error|info|debug|trace>)
133134
listen_uri: <listen_uri> # host:port
134135
profiles_path: <path_to_profile_file>
136+
use_forwarded_for_headers: <true|false>
135137
online_client:
136138
credentials_path: <path_to_crowdsec_api_client_credential_file>
137139
tls:
@@ -304,6 +306,7 @@ api:
304306
log_level: (error|info|debug|trace>)
305307
listen_uri: <listen_uri> # host:port
306308
profiles_path: <path_to_profile_file>
309+
use_forwarded_for_headers: (true|false)
307310
online_client:
308311
credentials_path: <path_to_crowdsec_api_client_credential_file>
309312
tls:
@@ -340,6 +343,7 @@ server:
340343
log_level: (error|info|debug|trace)
341344
listen_uri: <listen_uri> # host:port
342345
profiles_path: <path_to_profile_file>
346+
use_forwarded_for_headers: (true|false)
343347
online_client:
344348
credentials_path: <path_to_crowdsec_api_client_credential_file>
345349
tls:
@@ -357,6 +361,11 @@ Address and port listen configuration, the form `host:port`.
357361

358362
The path to the {{v1X.profiles.htmlname}} configuration.
359363

364+
#### `use_forwarded_for_headers`
365+
> string
366+
367+
Allow the usage of `X-Forwarded-For` or `X-Real-IP` to get the client IP address. Do not enable if you are not running the LAPI behind a trusted reverse-proxy or LB.
368+
360369
#### `online_client`
361370

362371
Configuration to push signals and receive bad IPs from Crowdsec API.

pkg/apiserver/apiserver.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ func NewServer(config *csconfig.LocalApiServerCfg) (*APIServer, error) {
6161
}
6262
log.Debugf("starting router, logging to %s", logFile)
6363
router := gin.New()
64-
/*related to https://github.com/gin-gonic/gin/pull/2474
65-
Gin team doesn't seem to be willing to have a opt-in/opt-out on the trusted proxies.
66-
For now, let's not trust that. */
67-
router.ForwardedByClientIP = false
64+
/* See https://github.com/gin-gonic/gin/pull/2474:
65+
Gin does not handle safely X-Forwarded-For or X-Real-IP.
66+
We do not trust them by default, but the user can opt-in
67+
if they host LAPI behind a trusted proxy which sanitize
68+
X-Forwarded-For and X-Real-IP.
69+
*/
70+
router.ForwardedByClientIP = config.UseForwardedForHeaders
6871

6972
/*The logger that will be used by handlers*/
7073
clog := log.New()

pkg/apiserver/apiserver_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,33 @@ func LoadTestConfig() csconfig.GlobalConfig {
5757
return config
5858
}
5959

60+
func LoadTestConfigForwardedFor() csconfig.GlobalConfig {
61+
config := csconfig.GlobalConfig{}
62+
maxAge := "1h"
63+
flushConfig := csconfig.FlushDBCfg{
64+
MaxAge: &maxAge,
65+
}
66+
dbconfig := csconfig.DatabaseCfg{
67+
Type: "sqlite",
68+
DbPath: "./ent",
69+
Flush: &flushConfig,
70+
}
71+
apiServerConfig := csconfig.LocalApiServerCfg{
72+
ListenURI: "http://127.0.0.1:8080",
73+
DbConfig: &dbconfig,
74+
ProfilesPath: "./tests/profiles.yaml",
75+
UseForwardedForHeaders: true,
76+
}
77+
apiConfig := csconfig.APICfg{
78+
Server: &apiServerConfig,
79+
}
80+
config.API = &apiConfig
81+
if err := config.API.Server.LoadProfiles(); err != nil {
82+
log.Fatalf("failed to load profiles: %s", err)
83+
}
84+
return config
85+
}
86+
6087
func NewAPITest() (*gin.Engine, error) {
6188
config := LoadTestConfig()
6289

@@ -74,6 +101,23 @@ func NewAPITest() (*gin.Engine, error) {
74101
return router, nil
75102
}
76103

104+
func NewAPITestForwardedFor() (*gin.Engine, error) {
105+
config := LoadTestConfigForwardedFor()
106+
107+
os.Remove("./ent")
108+
apiServer, err := NewServer(config.API.Server)
109+
if err != nil {
110+
return nil, fmt.Errorf("unable to run local API: %s", err)
111+
}
112+
log.Printf("Creating new API server")
113+
gin.SetMode(gin.TestMode)
114+
router, err := apiServer.Router()
115+
if err != nil {
116+
return nil, fmt.Errorf("unable to run local API: %s", err)
117+
}
118+
return router, nil
119+
}
120+
77121
func ValidateMachine(machineID string) error {
78122
config := LoadTestConfig()
79123
dbClient, err := database.NewClient(config.API.Server.DbConfig)
@@ -86,6 +130,24 @@ func ValidateMachine(machineID string) error {
86130
return nil
87131
}
88132

133+
func GetMachineIP(machineID string) (string, error) {
134+
config := LoadTestConfig()
135+
dbClient, err := database.NewClient(config.API.Server.DbConfig)
136+
if err != nil {
137+
return "", fmt.Errorf("unable to create new database client: %s", err)
138+
}
139+
machines, err := dbClient.ListMachines()
140+
if err != nil {
141+
return "", fmt.Errorf("Unable to list machines: %s", err)
142+
}
143+
for _, machine := range machines {
144+
if machine.MachineId == machineID {
145+
return machine.IpAddress, nil
146+
}
147+
}
148+
return "", nil
149+
}
150+
89151
func CreateTestMachine(router *gin.Engine) (string, error) {
90152
b, err := json.Marshal(MachineTest)
91153
if err != nil {

pkg/apiserver/machines_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,96 @@ func TestCreateMachine(t *testing.T) {
5252

5353
}
5454

55+
func TestCreateMachineWithForwardedFor(t *testing.T) {
56+
router, err := NewAPITestForwardedFor()
57+
if err != nil {
58+
log.Fatalf("unable to run local API: %s", err)
59+
}
60+
61+
// Create machine
62+
b, err := json.Marshal(MachineTest)
63+
if err != nil {
64+
log.Fatalf("unable to marshal MachineTest")
65+
}
66+
body := string(b)
67+
68+
w := httptest.NewRecorder()
69+
req, _ := http.NewRequest("POST", "/v1/watchers", strings.NewReader(body))
70+
req.Header.Add("User-Agent", UserAgent)
71+
req.Header.Add("X-Real-IP", "1.1.1.1")
72+
router.ServeHTTP(w, req)
73+
74+
assert.Equal(t, 201, w.Code)
75+
assert.Equal(t, "", w.Body.String())
76+
77+
ip, err := GetMachineIP(*MachineTest.MachineID)
78+
if err != nil {
79+
log.Fatalf("Could not get machine IP : %s", err)
80+
}
81+
assert.Equal(t, "1.1.1.1", ip)
82+
}
83+
84+
func TestCreateMachineWithForwardedForNoConfig(t *testing.T) {
85+
router, err := NewAPITest()
86+
if err != nil {
87+
log.Fatalf("unable to run local API: %s", err)
88+
}
89+
90+
// Create machine
91+
b, err := json.Marshal(MachineTest)
92+
if err != nil {
93+
log.Fatalf("unable to marshal MachineTest")
94+
}
95+
body := string(b)
96+
97+
w := httptest.NewRecorder()
98+
req, _ := http.NewRequest("POST", "/v1/watchers", strings.NewReader(body))
99+
req.Header.Add("User-Agent", UserAgent)
100+
req.Header.Add("X-Real-IP", "1.1.1.1")
101+
router.ServeHTTP(w, req)
102+
103+
assert.Equal(t, 201, w.Code)
104+
assert.Equal(t, "", w.Body.String())
105+
106+
ip, err := GetMachineIP(*MachineTest.MachineID)
107+
if err != nil {
108+
log.Fatalf("Could not get machine IP : %s", err)
109+
}
110+
//For some reason, the IP is empty when running tests
111+
//if no forwarded-for headers are present
112+
assert.Equal(t, "", ip)
113+
}
114+
115+
func TestCreateMachineWithoutForwardedFor(t *testing.T) {
116+
router, err := NewAPITestForwardedFor()
117+
if err != nil {
118+
log.Fatalf("unable to run local API: %s", err)
119+
}
120+
121+
// Create machine
122+
b, err := json.Marshal(MachineTest)
123+
if err != nil {
124+
log.Fatalf("unable to marshal MachineTest")
125+
}
126+
body := string(b)
127+
128+
w := httptest.NewRecorder()
129+
req, _ := http.NewRequest("POST", "/v1/watchers", strings.NewReader(body))
130+
req.Header.Add("User-Agent", UserAgent)
131+
router.ServeHTTP(w, req)
132+
133+
assert.Equal(t, 201, w.Code)
134+
assert.Equal(t, "", w.Body.String())
135+
136+
ip, err := GetMachineIP(*MachineTest.MachineID)
137+
if err != nil {
138+
log.Fatalf("Could not get machine IP : %s", err)
139+
}
140+
//For some reason, the IP is empty when running tests
141+
//if no forwarded-for headers are present
142+
assert.Equal(t, "", ip)
143+
}
144+
55145
func TestCreateMachineAlreadyExist(t *testing.T) {
56146
router, err := NewAPITest()
57147
if err != nil {

pkg/csconfig/api.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ type LocalApiClientCfg struct {
2828

2929
/*local api service configuration*/
3030
type LocalApiServerCfg struct {
31-
ListenURI string `yaml:"listen_uri,omitempty"` //127.0.0.1:8080
32-
TLS *TLSCfg `yaml:"tls"`
33-
DbConfig *DatabaseCfg `yaml:"-"`
34-
LogDir string `yaml:"-"`
35-
OnlineClient *OnlineApiClientCfg `yaml:"online_client"`
36-
ProfilesPath string `yaml:"profiles_path,omitempty"`
37-
Profiles []*ProfileCfg `yaml:"-"`
38-
LogLevel *log.Level `yaml:"log_level"`
31+
ListenURI string `yaml:"listen_uri,omitempty"` //127.0.0.1:8080
32+
TLS *TLSCfg `yaml:"tls"`
33+
DbConfig *DatabaseCfg `yaml:"-"`
34+
LogDir string `yaml:"-"`
35+
OnlineClient *OnlineApiClientCfg `yaml:"online_client"`
36+
ProfilesPath string `yaml:"profiles_path,omitempty"`
37+
Profiles []*ProfileCfg `yaml:"-"`
38+
LogLevel *log.Level `yaml:"log_level"`
39+
UseForwardedForHeaders bool `yaml:"use_forwarded_for_headers,omitempty"`
3940
}
4041

4142
type TLSCfg struct {

pkg/csconfig/config.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ func NewDefaultConfig() *GlobalConfig {
232232
CredentialsFilePath: "/etc/crowdsec/config/lapi-secrets.yaml",
233233
},
234234
Server: &LocalApiServerCfg{
235-
ListenURI: "127.0.0.1:8080",
235+
ListenURI: "127.0.0.1:8080",
236+
UseForwardedForHeaders: false,
236237
OnlineClient: &OnlineApiClientCfg{
237238
CredentialsFilePath: "/etc/crowdsec/config/online-api-secrets.yaml",
238239
},

0 commit comments

Comments
 (0)