Merge pull request #835 from cdr/log-failed-logins
Add failed authentication attempt logger
This commit is contained in:
commit
f25a614333
15
doc/security/code-server.fail2ban.conf
Normal file
15
doc/security/code-server.fail2ban.conf
Normal 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
42
doc/security/fail2ban.md
Normal 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.
|
||||||
|
|
@ -38,21 +38,24 @@ Usage: code-server [options]
|
|||||||
Run VS Code on a remote server.
|
Run VS Code on a remote server.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-V, --version output the version number
|
-V, --version output the version number
|
||||||
--cert <value>
|
--cert <value>
|
||||||
--cert-key <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.
|
||||||
-d, --user-data-dir <dir> Specifies the directory that user data is kept in, useful when running as root.
|
--extra-extensions-dir [dir] Path to an extra user extension directory (repeatable). (default: [])
|
||||||
--data-dir <value> DEPRECATED: Use '--user-data-dir' instead. Customize where user-data is stored.
|
--extra-builtin-extensions-dir [dir] Path to an extra built-in extension directory (repeatable). (default: [])
|
||||||
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
|
-d, --user-data-dir <dir> Specifies the directory that user data is kept in, useful when running as root.
|
||||||
-o, --open Open in the browser on startup.
|
-h, --host <value> Customize the hostname. (default: "0.0.0.0")
|
||||||
-p, --port <number> Port to bind on. (default: 8443)
|
-o, --open Open in the browser on startup.
|
||||||
-N, --no-auth Start without requiring authentication.
|
-p, --port <number> Port to bind on. (default: 8443)
|
||||||
-H, --allow-http Allow http connections.
|
-N, --no-auth Start without requiring authentication.
|
||||||
-P, --password <value> Specify a password for authentication.
|
-H, --allow-http Allow http connections.
|
||||||
--disable-telemetry Disables ALL telemetry.
|
--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
|
### 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.
|
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)
|
> 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 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 {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name code.example.com code.example.org;
|
server_name code.example.com code.example.org;
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:8443/;
|
proxy_pass http://localhost:8443/;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection upgrade;
|
proxy_set_header Connection upgrade;
|
||||||
proxy_set_header Accept-Encoding gzip;
|
proxy_set_header Accept-Encoding gzip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Apache Reverse Proxy
|
### 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>
|
<VirtualHost *:80>
|
||||||
ServerName code.example.com
|
ServerName code.example.com
|
||||||
|
@ -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("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
|
||||||
.option("--disable-telemetry", "Disables ALL telemetry.", false)
|
.option("--disable-telemetry", "Disables ALL telemetry.", false)
|
||||||
.option("--socket <value>", "Listen on a UNIX socket. Host and port will be ignored when set.")
|
.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("--install-extension <value>", "Install an extension by its ID.")
|
||||||
.option("--bootstrap-fork <name>", "Used for development. Never set.")
|
.option("--bootstrap-fork <name>", "Used for development. Never set.")
|
||||||
.option("--extra-args <args>", "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 cert?: string;
|
||||||
readonly certKey?: string;
|
readonly certKey?: string;
|
||||||
readonly socket?: string;
|
readonly socket?: string;
|
||||||
|
readonly trustProxy?: boolean;
|
||||||
|
|
||||||
readonly installExtension?: string;
|
readonly installExtension?: string;
|
||||||
|
|
||||||
@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
password,
|
password,
|
||||||
|
trustProxy: options.trustProxy,
|
||||||
httpsOptions: hasCustomHttps ? {
|
httpsOptions: hasCustomHttps ? {
|
||||||
key: certKeyData,
|
key: certKeyData,
|
||||||
cert: certData,
|
cert: certData,
|
||||||
|
@ -31,6 +31,7 @@ interface CreateAppOptions {
|
|||||||
httpsOptions?: https.ServerOptions;
|
httpsOptions?: https.ServerOptions;
|
||||||
allowHttp?: boolean;
|
allowHttp?: boolean;
|
||||||
bypassAuth?: boolean;
|
bypassAuth?: boolean;
|
||||||
|
trustProxy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createApp = async (options: CreateAppOptions): Promise<{
|
export const createApp = async (options: CreateAppOptions): Promise<{
|
||||||
@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
|||||||
return true;
|
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 => {
|
const isAuthed = (req: http.IncomingMessage): boolean => {
|
||||||
try {
|
try {
|
||||||
if (!options.password || options.bypassAuth) {
|
if (!options.password || options.bypassAuth) {
|
||||||
@ -70,7 +86,22 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
|||||||
|
|
||||||
// Try/catch placed here just in case
|
// Try/catch placed here just in case
|
||||||
const cookies = parseCookies(req);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@ -214,7 +245,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{
|
|||||||
const staticGzip = expressStaticGzip(path.join(baseDir, "build/web"));
|
const staticGzip = expressStaticGzip(path.join(baseDir, "build/web"));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
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.
|
// Force HTTPS unless allowing HTTP.
|
||||||
if (!isEncrypted(req.socket) && !options.allowHttp) {
|
if (!isEncrypted(req.socket) && !options.allowHttp) {
|
||||||
|
Reference in New Issue
Block a user