Make it possible to secure n8n via basic auth

This commit is contained in:
Jan Oberhauser 2019-08-04 14:24:48 +02:00
parent a8b2829e84
commit f3d84fc29e
7 changed files with 223 additions and 86 deletions

View file

@ -53,6 +53,22 @@ docker run -it --rm \
n8n start --tunnel
```
## Securing n8n
By default n8n can be accessed by everybody. This is OK if you have it only running
locally buy if you deploy it on a server which is accessible from the web you have
to make sure that n8n is protected!
Right now we have very basic protection via basic-auth in place. It can be activated
by setting the following environment variables:
```
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=<USER>
N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
```
## Persist data
The workflow data gets by default saved in an SQLite database in the user
@ -73,11 +89,6 @@ By default n8n uses SQLite to save credentials, past executions and workflows.
n8n however also supports MongoDB and PostgresDB. To use them simply a few
environment variables have to be set.
To avoid passing sensitive information via environment variables "_FILE" may be
appended to the database environment variables (for example "DB_POSTGRESDB_PASSWORD_FILE").
It will then load the data from a file with the given name. That makes it possible to
load data easily from Docker- and Kubernetes-Secrets.
It is important to still persist the data in the `/root/.n8` folder. The reason
is that it contains n8n user data. That is the name of the webhook
(in case) the n8n tunnel gets used and even more important the encryption key
@ -133,6 +144,25 @@ docker run -it --rm \
n8n start
```
## Passing Senstive Data via File
To avoid passing sensitive information via environment variables "_FILE" may be
appended to some environment variables. It will then load the data from a file
with the given name. That makes it possible to load data easily from
Docker- and Kubernetes-Secrets.
The following environment variables support file input:
- DB_MONGODB_CONNECTION_URL
- DB_POSTGRESDB_DATABASE_FILE
- DB_POSTGRESDB_HOST_FILE
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE
## License
n8n is licensed under **Apache 2.0 with Commons Clause**

View file

@ -67,6 +67,20 @@ To use it simply start n8n with `--tunnel`
n8n start --tunnel
```
### Securing n8n
By default n8n can be accessed by everybody. This is OK if you have it only running
locally buy if you deploy it on a server which is accessible from the web you have
to make sure that n8n is protected!
Right now we have very basic protection via basic-auth in place. It can be activated
by setting the following environment variables:
```
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=<USER>
N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
```
### Start with other Database
@ -74,11 +88,6 @@ By default n8n uses SQLite to save credentials, past executions and workflows.
n8n however also supports MongoDB and PostgresDB. To use them simply a few
environment variables have to be set.
To avoid passing sensitive information via environment variables "_FILE" may be
appended to the database environment variables (for example "DB_POSTGRESDB_PASSWORD_FILE").
It will then load the data from a file with the given name. That makes it possible to
load data easily from Docker- and Kubernetes-Secrets.
#### Start with MongoDB as Database
@ -125,6 +134,24 @@ n8n start
```
## Passing Senstive Data via File
To avoid passing sensitive information via environment variables "_FILE" may be
appended to some environment variables. It will then load the data from a file
with the given name. That makes it possible to load data easily from
Docker- and Kubernetes-Secrets.
The following environment variables support file input:
- DB_MONGODB_CONNECTION_URL
- DB_POSTGRESDB_DATABASE_FILE
- DB_POSTGRESDB_HOST_FILE
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE
## Execute Workflow from CLI
Workflows can not just be started by triggers, webhooks or manually via the

View file

@ -23,7 +23,7 @@ import { promisify } from "util";
const tunnel = promisify(localtunnel);
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;
/**
* Opens the UI in browser
@ -68,6 +68,7 @@ flag "--init" to fix this problem!`);
// Wrap that the process does not close but we can still use async
(async () => {
try {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init();
@ -114,7 +115,7 @@ flag "--init" to fix this problem!`);
console.log(`Tunnel URL: ${process.env.WEBHOOK_TUNNEL_URL}\n`);
}
Server.start();
await Server.start();
// Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
@ -152,6 +153,13 @@ flag "--init" to fix this problem!`);
}
});
}
} catch (error) {
console.error(`There was an error: ${error.message}`);
processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
}
})();
@ -161,7 +169,7 @@ flag "--init" to fix this problem!`);
setTimeout(() => {
// In case that something goes wrong with shutdown we
// kill after max. 30 seconds no matter what
process.exit();
process.exit(processExistCode);
}, 30000);
const removePromises = [];
@ -175,7 +183,7 @@ flag "--init" to fix this problem!`);
await Promise.all(removePromises);
process.exit();
process.exit(processExistCode);
});
});
};

View file

@ -121,6 +121,30 @@ const config = convict({
doc: 'HTTP Protocol via which n8n can be reached'
},
security: {
basicAuth: {
active: {
format: 'Boolean',
default: false,
env: 'N8N_BASIC_AUTH_ACTIVE',
doc: 'If basic auth should be activated for editor and REST-API'
},
user: {
format: String,
default: '',
env: 'N8N_BASIC_AUTH_USER',
doc: 'The name of the basic auth user'
},
password: {
format: String,
default: '',
env: 'N8N_BASIC_AUTH_PASSWORD',
doc: 'The password of the basic auth user'
},
}
},
endpoints: {
rest: {
format: String,

View file

@ -39,6 +39,7 @@
"dist"
],
"devDependencies": {
"@types/basic-auth": "^1.1.2",
"@types/compression": "0.0.36",
"@types/connect-history-api-fallback": "^1.3.1",
"@types/convict": "^4.2.1",
@ -59,6 +60,7 @@
"typescript": "~3.5.2"
},
"dependencies": {
"basic-auth": "^2.0.1",
"body-parser": "^1.18.3",
"compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0",

View file

@ -45,6 +45,14 @@ export class ReponseError extends Error {
}
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
resp.end(message);
}
export function sendSuccessResponse(res: Response, data: any, raw?: boolean) { // tslint:disable-line:no-any
res.setHeader('Content-Type', 'application/json');

View file

@ -61,11 +61,12 @@ import {
Not,
} from 'typeorm';
import * as parseUrl from 'parseurl';
import * as basicAuth from 'basic-auth';
import * as compression from 'compression';
import * as config from '../config';
// @ts-ignore
import * as timezones from 'google-timezones-json';
import * as compression from 'compression';
import * as parseUrl from 'parseurl';
class App {
@ -92,7 +93,6 @@ class App {
this.saveManualExecutions = config.get('executions.saveDataManualExecutions') as boolean;
this.timezone = config.get('generic.timezone') as string;
this.config();
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
this.testWebhooks = TestWebhooks.getInstance();
this.push = Push.getInstance();
@ -112,8 +112,44 @@ class App {
}
private config(): void {
async config(): Promise<void> {
// Check for basic auth credentials if activated
const basicAuthActive = config.get('security.basicAuth.active') as boolean;
if (basicAuthActive === true) {
const basicAuthUser = await GenericHelpers.getConfigValue('security.basicAuth.user') as string;
if (basicAuthUser === '') {
throw new Error('Basic auth is activated but no user got defined. Please set one!');
}
const basicAuthPassword = await GenericHelpers.getConfigValue('security.basicAuth.password') as string;
if (basicAuthPassword === '') {
throw new Error('Basic auth is activated but no password got defined. Please set one!');
}
const authIgnoreRegex = new RegExp(`^\/(rest|${this.endpointWebhook}|${this.endpointWebhookTest})\/.*$`)
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.url.match(authIgnoreRegex)) {
return next();
}
const realm = 'n8n - Editor UI';
const basicAuthData = basicAuth(req);
if (basicAuthData === undefined) {
// Authorization data is missing
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization is required!');
}
if (basicAuthData.name !== basicAuthUser || basicAuthData.pass !== basicAuthPassword) {
// Provided authentication data is wrong
return ResponseHelper.basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!');
}
next();
});
}
// Compress the repsonse data
this.app.use(compression());
// Get push connections
@ -1036,12 +1072,14 @@ class App {
}
export function start() {
export async function start(): Promise<void> {
const PORT = config.get('port');
const app = new App().app;
const app = new App();
app.listen(PORT, () => {
await app.config();
app.app.listen(PORT, () => {
console.log('n8n ready on port ' + PORT);
});
}