diff --git a/doc/security/code-server.fail2ban.conf b/doc/security/code-server.fail2ban.conf new file mode 100644 index 00000000..3f4edae4 --- /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 00000000..a9ad8681 --- /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/doc/self-hosted/index.md b/doc/self-hosted/index.md index ffa0b735..f3a867f1 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. @@ -79,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 diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index c0f896ff..3f7db3e8 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 b7d9a12c..70dbb765 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,22 @@ 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"]; + 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("timestamp", timestamp)); + + return false; + } + return true; } } catch (ex) { @@ -214,7 +245,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) {