Skip to content

Commit f25a614

Browse files
authored
Merge pull request coder#835 from cdr/log-failed-logins
Add failed authentication attempt logger
2 parents 72d7166 + 0ae8c18 commit f25a614

File tree

5 files changed

+120
-24
lines changed

5 files changed

+120
-24
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Fail2Ban filter for code-server
2+
#
3+
#
4+
5+
[Definition]
6+
7+
8+
failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remote_address\":\"<HOST>\"
9+
10+
ignoreregex =
11+
12+
datepattern = "timestamp":{EPOCH}}$
13+
14+
# Author: Dean Sheather
15+

doc/security/fail2ban.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Protecting code-server from bruteforce attempts
2+
3+
code-server outputs all failed login attempts, along with the IP address,
4+
provided password, user agent and timestamp by default. When using a reverse
5+
proxy such as Nginx or Apache, the remote address may appear to be `127.0.0.1`
6+
or a similar address unless the `--trust-proxy` argument is provided to
7+
code-server.
8+
9+
When used with the `--trust-proxy` argument, code-server will use the last IP in
10+
`X-Forwarded-For` (if provided) instead of the remote socket address. Ensure
11+
that you are setting this value in your reverse proxy:
12+
13+
Nginx:
14+
```
15+
location / {
16+
...
17+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18+
...
19+
}
20+
```
21+
22+
Apache:
23+
```
24+
<VirtualEnv>
25+
...
26+
SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded
27+
...
28+
</VirtualEnv>
29+
```
30+
31+
It is extremely important that if you enable `--trust-proxy` you ensure your
32+
code-server instance is not accessible from the internet (block it in your
33+
firewall).
34+
35+
## Fail2Ban
36+
37+
Fail2Ban allows for automatically banning and logging repeated failed
38+
authentication attempts for many applications through regex filters. A working
39+
filter for code-server can be found in `./code-server.fail2ban.conf`. Once this
40+
is installed and configured correctly, repeated failed login attempts should
41+
automatically be banned from connecting to your server.
42+

doc/self-hosted/index.md

+25-22
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,24 @@ Usage: code-server [options]
3838
Run VS Code on a remote server.
3939
4040
Options:
41-
-V, --version output the version number
41+
-V, --version output the version number
4242
--cert <value>
4343
--cert-key <value>
44-
-e, --extensions-dir <dir> Set the root path for extensions.
45-
-d, --user-data-dir <dir> Specifies the directory that user data is kept in, useful when running as root.
46-
--data-dir <value> DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.
47-
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
48-
-o, --open Open in the browser on startup.
49-
-p, --port <number> Port to bind on. (default: 8443)
50-
-N, --no-auth Start without requiring authentication.
51-
-H, --allow-http Allow http connections.
52-
-P, --password <value> Specify a password for authentication.
53-
--disable-telemetry Disables ALL telemetry.
54-
--help output usage information
55-
```
44+
-e, --extensions-dir <dir> Override the main default path for user extensions.
45+
--extra-extensions-dir [dir] Path to an extra user extension directory (repeatable). (default: [])
46+
--extra-builtin-extensions-dir [dir] Path to an extra built-in extension directory (repeatable). (default: [])
47+
-d, --user-data-dir <dir> Specifies the directory that user data is kept in, useful when running as root.
48+
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
49+
-o, --open Open in the browser on startup.
50+
-p, --port <number> Port to bind on. (default: 8443)
51+
-N, --no-auth Start without requiring authentication.
52+
-H, --allow-http Allow http connections.
53+
--disable-telemetry Disables ALL telemetry.
54+
--socket <value> Listen on a UNIX socket. Host and port will be ignored when set.
55+
--trust-proxy Trust the X-Forwarded-For header, useful when using a reverse proxy.
56+
--install-extension <value> Install an extension by its ID.
57+
-h, --help output usage information
58+
```
5659

5760
### Data Directory
5861
Use `code-server -d (path/to/directory)` or `code-server --user-data-dir=(path/to/directory)`, excluding the parentheses to specify the root folder that VS Code will start in.
@@ -79,23 +82,23 @@ Options:
7982
> To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md)
8083
8184
### Nginx Reverse Proxy
82-
Nginx is for reverse proxy. Below is a virtual host example that works with code-server. Please also pass --allow-http. You can also use certbot by EFF to get a ssl certificates for free.
85+
Below is a virtual host example that works with code-server. Please also pass `--allow-http` and `--trust-proxy` to code-server to allow the proxy to connect. You can also use Let's Encrypt to get a SSL certificates for free.
8386
```
8487
server {
8588
listen 80;
8689
listen [::]:80;
8790
server_name code.example.com code.example.org;
88-
location / {
89-
proxy_pass http://localhost:8443/;
90-
proxy_set_header Upgrade $http_upgrade;
91-
proxy_set_header Connection upgrade;
92-
proxy_set_header Accept-Encoding gzip;
93-
}
94-
}
91+
location / {
92+
proxy_pass http://localhost:8443/;
93+
proxy_set_header Upgrade $http_upgrade;
94+
proxy_set_header Connection upgrade;
95+
proxy_set_header Accept-Encoding gzip;
96+
}
97+
}
9598
```
9699

97100
### Apache Reverse Proxy
98-
Example of https virtualhost configuration for Apache as a reverse proxy. Please also pass --allow-http on code-server startup to allow the proxy to connect.
101+
Example of a HTTPS virtualhost configuration for Apache as a reverse proxy. Please also pass `--allow-http` and `--trust-proxy` to code-server to allow the proxy to connect. You can also use Let's Encrypt to get a SSL certificates for free.
99102
```
100103
<VirtualHost *:80>
101104
ServerName code.example.com

packages/server/src/cli.ts

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development")
3838
.option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
3939
.option("--disable-telemetry", "Disables ALL telemetry.", false)
4040
.option("--socket <value>", "Listen on a UNIX socket. Host and port will be ignored when set.")
41+
.option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false)
4142
.option("--install-extension <value>", "Install an extension by its ID.")
4243
.option("--bootstrap-fork <name>", "Used for development. Never set.")
4344
.option("--extra-args <args>", "Used for development. Never set.")
@@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => {
7475
readonly cert?: string;
7576
readonly certKey?: string;
7677
readonly socket?: string;
78+
readonly trustProxy?: boolean;
7779

7880
readonly installExtension?: string;
7981

@@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => {
273275
},
274276
},
275277
password,
278+
trustProxy: options.trustProxy,
276279
httpsOptions: hasCustomHttps ? {
277280
key: certKeyData,
278281
cert: certData,

packages/server/src/server.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface CreateAppOptions {
3131
httpsOptions?: https.ServerOptions;
3232
allowHttp?: boolean;
3333
bypassAuth?: boolean;
34+
trustProxy?: boolean;
3435
}
3536

3637
export const createApp = async (options: CreateAppOptions): Promise<{
@@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{
6263
return true;
6364
};
6465

66+
const remoteAddress = (req: http.IncomingMessage): string | void => {
67+
let xForwardedFor = req.headers["x-forwarded-for"];
68+
if (Array.isArray(xForwardedFor)) {
69+
xForwardedFor = xForwardedFor.join(", ");
70+
}
71+
72+
if (options.trustProxy && xForwardedFor !== undefined) {
73+
const addresses = xForwardedFor.split(",").map(s => s.trim());
74+
75+
return addresses.pop();
76+
}
77+
78+
return req.socket.remoteAddress;
79+
};
80+
6581
const isAuthed = (req: http.IncomingMessage): boolean => {
6682
try {
6783
if (!options.password || options.bypassAuth) {
@@ -70,7 +86,22 @@ export const createApp = async (options: CreateAppOptions): Promise<{
7086

7187
// Try/catch placed here just in case
7288
const cookies = parseCookies(req);
73-
if (cookies.password && safeCompare(cookies.password, options.password)) {
89+
if (cookies.password) {
90+
if (!safeCompare(cookies.password, options.password)) {
91+
let userAgent = req.headers["user-agent"];
92+
let timestamp = Math.floor(new Date().getTime() / 1000);
93+
if (Array.isArray(userAgent)) {
94+
userAgent = userAgent.join(", ");
95+
}
96+
logger.info("Failed login attempt",
97+
field("password", cookies.password),
98+
field("remote_address", remoteAddress(req)),
99+
field("user_agent", userAgent),
100+
field("timestamp", timestamp));
101+
102+
return false;
103+
}
104+
74105
return true;
75106
}
76107
} catch (ex) {
@@ -214,7 +245,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{
214245
const staticGzip = expressStaticGzip(path.join(baseDir, "build/web"));
215246

216247
app.use((req, res, next) => {
217-
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip));
248+
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`,
249+
field("host", req.hostname),
250+
field("remote_address", remoteAddress(req)));
218251

219252
// Force HTTPS unless allowing HTTP.
220253
if (!isEncrypted(req.socket) && !options.allowHttp) {

0 commit comments

Comments
 (0)