Merge pull request #835 from cdr/log-failed-logins

Add failed authentication attempt logger
This commit is contained in:
Dean Sheather 2019-07-11 02:42:55 +00:00 committed by GitHub
commit f25a614333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 24 deletions

View File

@ -0,0 +1,15 @@
# Fail2Ban filter for code-server
#
#
[Definition]
failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remote_address\":\"<HOST>\"
ignoreregex =
datepattern = "timestamp":{EPOCH}}$
# Author: Dean Sheather

42
doc/security/fail2ban.md Normal file
View File

@ -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:
```
<VirtualEnv>
...
SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded
...
</VirtualEnv>
```
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.

View File

@ -41,18 +41,21 @@ Options:
-V, --version output the version number
--cert <value>
--cert-key <value>
-e, --extensions-dir <dir> Set the root path for extensions.
-e, --extensions-dir <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 <dir> Specifies the directory that user data is kept in, useful when running as root.
--data-dir <value> DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
-o, --open Open in the browser on startup.
-p, --port <number> Port to bind on. (default: 8443)
-N, --no-auth Start without requiring authentication.
-H, --allow-http Allow http connections.
-P, --password <value> Specify a password for authentication.
--disable-telemetry Disables ALL telemetry.
--help output usage information
```
--socket <value> 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 <value> 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,7 +82,7 @@ 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;
@ -95,7 +98,7 @@ Options:
```
### 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.
```
<VirtualHost *:80>
ServerName code.example.com

View File

@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development")
.option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
.option("--disable-telemetry", "Disables ALL telemetry.", false)
.option("--socket <value>", "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 <value>", "Install an extension by its ID.")
.option("--bootstrap-fork <name>", "Used for development. Never set.")
.option("--extra-args <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,

View File

@ -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) {