From a65773338c2bf140a4731a0ac10ac80694b6a831 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 7 Jul 2019 16:50:43 +1000 Subject: [PATCH 1/4] add failed authentication attempt logger When `isAuthed()` is called and the password cookie is not what we expected, the failed login attempt is logged with the provided password, remote address and user agent. To allow for logging failed attempts with a reverse proxy, the `--trust-proxy` argument has been added to trust the `X-Forwarded-For` header. This implementation of an `X-Forwarded-For` parser uses the last value in the list, therefore only trusting the nearest proxy. --- packages/server/src/cli.ts | 3 +++ packages/server/src/server.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 8433a4033..7f43212a6 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development") .option("-P, --password ", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.") .option("--disable-telemetry", "Disables ALL telemetry.", false) .option("--socket ", "Listen on a UNIX socket. Host and port will be ignored when set.") + .option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false) .option("--install-extension ", "Install an extension by its ID.") .option("--bootstrap-fork ", "Used for development. Never set.") .option("--extra-args ", "Used for development. Never set.") @@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => { readonly cert?: string; readonly certKey?: string; readonly socket?: string; + readonly trustProxy?: boolean; readonly installExtension?: string; @@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => { }, }, password, + trustProxy: options.trustProxy, httpsOptions: hasCustomHttps ? { key: certKeyData, cert: certData, diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index b7d9a12cd..4c73743ad 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -31,6 +31,7 @@ interface CreateAppOptions { httpsOptions?: https.ServerOptions; allowHttp?: boolean; bypassAuth?: boolean; + trustProxy?: boolean; } export const createApp = async (options: CreateAppOptions): Promise<{ @@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{ return true; }; + const remoteAddress = (req: http.IncomingMessage): string | void => { + let xForwardedFor = req.headers["x-forwarded-for"]; + if (Array.isArray(xForwardedFor)) { + xForwardedFor = xForwardedFor.join(", "); + } + + if (options.trustProxy && xForwardedFor !== undefined) { + const addresses = xForwardedFor.split(",").map(s => s.trim()); + + return addresses.pop(); + } + + return req.socket.remoteAddress; + }; + const isAuthed = (req: http.IncomingMessage): boolean => { try { if (!options.password || options.bypassAuth) { @@ -70,7 +86,20 @@ export const createApp = async (options: CreateAppOptions): Promise<{ // Try/catch placed here just in case const cookies = parseCookies(req); - if (cookies.password && safeCompare(cookies.password, options.password)) { + if (cookies.password) { + if (!safeCompare(cookies.password, options.password)) { + let userAgent = req.headers["user-agent"]; + if (Array.isArray(userAgent)) { + userAgent = userAgent.join(", "); + } + logger.info("Failed login attempt", + field("password", cookies.password), + field("remote_address", remoteAddress(req)), + field("user_agent", userAgent)); + + return false; + } + return true; } } catch (ex) { @@ -214,7 +243,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{ const staticGzip = expressStaticGzip(path.join(baseDir, "build/web")); app.use((req, res, next) => { - logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip)); + logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, + field("host", req.hostname), + field("remote_address", remoteAddress(req))); // Force HTTPS unless allowing HTTP. if (!isEncrypted(req.socket) && !options.allowHttp) { From 14d917179c092b46526815e16abdd25fd1715397 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 7 Jul 2019 17:03:47 +1000 Subject: [PATCH 2/4] update code-server usage in documentation --- doc/self-hosted/index.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/doc/self-hosted/index.md b/doc/self-hosted/index.md index 17bf6c96c..e75d88099 100644 --- a/doc/self-hosted/index.md +++ b/doc/self-hosted/index.md @@ -38,21 +38,24 @@ Usage: code-server [options] Run VS Code on a remote server. Options: - -V, --version output the version number + -V, --version output the version number --cert --cert-key - -e, --extensions-dir Set the root path for extensions. - -d --user-data-dir Specifies the directory that user data is kept in, useful when running as root. - --data-dir DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored. - -h, --host Customize the hostname. (default: "0.0.0.0") - -o, --open Open in the browser on startup. - -p, --port Port to bind on. (default: 8443) - -N, --no-auth Start without requiring authentication. - -H, --allow-http Allow http connections. - -P, --password Specify a password for authentication. - --disable-telemetry Disables ALL telemetry. - --help output usage information - ``` + -e, --extensions-dir Override the main default path for user extensions. + --extra-extensions-dir [dir] Path to an extra user extension directory (repeatable). (default: []) + --extra-builtin-extensions-dir [dir] Path to an extra built-in extension directory (repeatable). (default: []) + -d --user-data-dir Specifies the directory that user data is kept in, useful when running as root. + -h, --host Customize the hostname. (default: "0.0.0.0") + -o, --open Open in the browser on startup. + -p, --port Port to bind on. (default: 8443) + -N, --no-auth Start without requiring authentication. + -H, --allow-http Allow http connections. + --disable-telemetry Disables ALL telemetry. + --socket Listen on a UNIX socket. Host and port will be ignored when set. + --trust-proxy Trust the X-Forwarded-For header, useful when using a reverse proxy. + --install-extension Install an extension by its ID. + -h, --help output usage information +``` ### Data Directory 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. From c48a275d33ff0be181dbf88aae21091721cab121 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 9 Jul 2019 10:57:09 +1000 Subject: [PATCH 3/4] add timestamp to auth log, add fail2ban conf+docs --- doc/security/code-server.fail2ban.conf | 15 +++++++++ doc/security/fail2ban.md | 42 ++++++++++++++++++++++++++ packages/server/src/server.ts | 4 ++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 doc/security/code-server.fail2ban.conf create mode 100644 doc/security/fail2ban.md diff --git a/doc/security/code-server.fail2ban.conf b/doc/security/code-server.fail2ban.conf new file mode 100644 index 000000000..3f4edae42 --- /dev/null +++ b/doc/security/code-server.fail2ban.conf @@ -0,0 +1,15 @@ +# Fail2Ban filter for code-server +# +# + +[Definition] + + +failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remote_address\":\"\" + +ignoreregex = + +datepattern = "timestamp":{EPOCH}}$ + +# Author: Dean Sheather + diff --git a/doc/security/fail2ban.md b/doc/security/fail2ban.md new file mode 100644 index 000000000..a9ad86810 --- /dev/null +++ b/doc/security/fail2ban.md @@ -0,0 +1,42 @@ +# Protecting code-server from bruteforce attempts + +code-server outputs all failed login attempts, along with the IP address, +provided password, user agent and timestamp by default. When using a reverse +proxy such as Nginx or Apache, the remote address may appear to be `127.0.0.1` +or a similar address unless the `--trust-proxy` argument is provided to +code-server. + +When used with the `--trust-proxy` argument, code-server will use the last IP in +`X-Forwarded-For` (if provided) instead of the remote socket address. Ensure +that you are setting this value in your reverse proxy: + +Nginx: +``` +location / { + ... + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ... +} +``` + +Apache: +``` + + ... + SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded + ... + +``` + +It is extremely important that if you enable `--trust-proxy` you ensure your +code-server instance is not accessible from the internet (block it in your +firewall). + +## Fail2Ban + +Fail2Ban allows for automatically banning and logging repeated failed +authentication attempts for many applications through regex filters. A working +filter for code-server can be found in `./code-server.fail2ban.conf`. Once this +is installed and configured correctly, repeated failed login attempts should +automatically be banned from connecting to your server. + diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 4c73743ad..70dbb7654 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -89,13 +89,15 @@ export const createApp = async (options: CreateAppOptions): Promise<{ if (cookies.password) { if (!safeCompare(cookies.password, options.password)) { let userAgent = req.headers["user-agent"]; + let timestamp = Math.floor(new Date().getTime() / 1000); if (Array.isArray(userAgent)) { userAgent = userAgent.join(", "); } logger.info("Failed login attempt", field("password", cookies.password), field("remote_address", remoteAddress(req)), - field("user_agent", userAgent)); + field("user_agent", userAgent), + field("timestamp", timestamp)); return false; } From e776f1819231207b1c84da5124188485207081f4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 11 Jul 2019 12:30:42 +1000 Subject: [PATCH 4/4] update docs to mention --trust-proxy --- doc/self-hosted/index.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/self-hosted/index.md b/doc/self-hosted/index.md index e75d88099..0a1b20e0a 100644 --- a/doc/self-hosted/index.md +++ b/doc/self-hosted/index.md @@ -82,23 +82,23 @@ Options: > To ensure the connection between you and your server is encrypted view our guide on [securing your setup](../security/ssl.md) ### Nginx Reverse Proxy - 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. + 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. ``` server { listen 80; listen [::]:80; server_name code.example.com code.example.org; - location / { - proxy_pass http://localhost:8443/; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection upgrade; - proxy_set_header Accept-Encoding gzip; - } - } + location / { + proxy_pass http://localhost:8443/; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection upgrade; + proxy_set_header Accept-Encoding gzip; + } + } ``` ### Apache Reverse Proxy - 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. + 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. ``` ServerName code.example.com