🔀 Merge pull request #1390 from toddejohnson/REFACTOR/2.1.2_unified-config

Attempt at adding header auth. Ignore Settings #981
This commit is contained in:
Alicia Sykes 2023-12-17 21:30:43 +00:00 committed by GitHub
commit 2d350ae7f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 309 additions and 95 deletions

View File

@ -194,6 +194,147 @@ Your app is now secured :) When you load Dashy, it will redirect to your Keycloa
From within the Keycloak console, you can then configure things like time-outs, password policies, etc. You can also backup your full Keycloak config, and it is recommended to do this, along with your Dashy config. You can spin up both Dashy and Keycloak simultaneously and restore both applications configs using a `docker-compose.yml` file, and this is recommended.
**[⬆️ Back to Top](#authentication)**
---
## Header Authentciation
### 1. Web Server/Reverse Proxy
Most web servers make password protecting certain apps very easy. Note that you should also set up HTTPS and have a valid certificate in order for this to be secure.
These are only an example. Please refer to your web server/reverse proxy for how to implement basic auth, set the correct headers, and ensure the headers aren't being forwarded from the client where they could be spoofed.
#### Authelia
[Authelia](https://www.authelia.com/) is an open-source full-featured authentication server, which can be self-hosted and either on bare metal, in a Docker container or in a Kubernetes cluster. It allows for fine-grained access control rules based on IP, path, users etc, and supports 2FA, simple password access or bypass policies for your domains.
- `git clone https://github.com/authelia/authelia.git`
- `cd authelia/examples/compose/lite`
- Modify the `users_database.yml` the default username and password is authelia
- Modify the `configuration.yml` and `docker-compose.yml` with your respective domains and secrets
- `docker-compose up -d`
For more information, see the [Authelia docs](https://www.authelia.com/docs/)
#### Apache
First crate a `.htaccess` file in Dashy's route directory. Specify the auth type and path to where you want to store the password file (usually the same folder). For example:
```text
AuthType Basic
AuthName "Please Sign into Dashy"
AuthUserFile /path/dashy/.htpasswd
require valid-user
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
RequestHeader set X-Remote-User expr=%{REMOTE_USER}
```
Then create a `.htpasswd` file in the same directory. List users and their hashed passwords here, with one user on each line, and a colon between username and password (e.g. `[username]:[hashed-password]`). You will need to generate an MD5 hash of your desired password, this can be done with an [online tool](https://www.web2generators.com/apache-tools/htpasswd-generator). Your file will look something like:
```text
alicia:$apr1$jv0spemw$RzOX5/GgY69JMkgV6u16l0
```
This will use the X-Remote-User header. This can also be expanded by [mod_auth_ldap, mod_auth_digest, mod_auth_mellon, ...](https://httpd.apache.org/docs/2.4/mod/#A)
#### NGINX
NGINX has an [authentication module](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) which can be used to add passwords to given sites, and is fairly simple to set up. Similar to above, you will need to create a `.htpasswd` file. Then just enable auth and specify the path to that file, for example:
```text
location / {
auth_basic "closed site";
auth_basic_user_file conf/htpasswd;
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Remote-User $remote_user;
}
```
#### Caddy
Caddy has a [basic-auth](https://caddyserver.com/docs/caddyfile/directives/basicauth) directive, where you specify a username and hash. The password hash needs to be base-64 encoded, the [`caddy hash-password`](https://caddyserver.com/docs/command-line#caddy-hash-password) command can help with this. For example:
```text
reverse_proxy localhost:8080 {
header_up -Authorization
header_up +X-Remote-User {username}
}
basicauth /secret/* {
alicia JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
}
```
For more info about implementing a single sign on for all your apps with Caddy, see [this tutorial](https://joshstrange.com/securing-your-self-hosted-apps-with-single-signon/)
#### Lighttpd
You can use the [mod_auth](https://doc.lighttpd.net/lighttpd2/mod_auth.html) module to secure your site with Lighttpd. Like with Apache, you need to first create a password file listing your usernames and hashed passwords, but in Lighttpd, it's usually called `.lighttpdpassword`.
Then in your `lighttpd.conf` file (usually in the `/etc/lighttpd/` directory), load in the mod_auth module, and configure it's directives. For example:
```text
server.modules += ( "mod_auth","mod_proxy" )
auth.debug = 2
auth.backend = "plain"
auth.backend.plain.userfile = "/home/lighttpd/.lighttpdpassword"
$HTTP["host"] == "dashy.my-domain.net" {
server.document-root = "/home/lighttpd/dashy.my-domain.net/http"
server.errorlog = "/var/log/lighttpd/dashy.my-domain.net/error.log"
accesslog.filename = "/var/log/lighttpd/dashy.my-domain.net/access.log"
auth.require = (
"" => (
"method" => "basic",
"realm" => "Password protected area",
"require" => "user=alicia"
)
)
proxy.server = ("" => (( "host" => "127.0.0.1", "port" => 8080 )))
proxy.forwarded = ( "for" => 1,
"proto" => 1,
"remote_user" => 1
)
}
```
Restart your web server for changes to take effect.
### 2. Dashy Configuration
With Header Authentication the web server/reverse proxy can add headers that Dashy will trust to be the valid authenticated user. You need to understand how your web server or reverse proxy works and trust it to add these details.
Even though we are trusting the web server for authentication we still use the Dashy user system for groups and rights. [See Setting Up Authentication](#setting-up-authentication) if you haven't set that up yet.
Below is the recomended Header Auth Configuration including some users.
```yaml
appConfig:
...
auth:
users:
- user: alicia
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
type: admin
- user: bob
hash: 5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
enableHeaderAuth: true
headerAuth:
userHeader: X-Remote-User
proxyWhitelist:
- 127.0.0.1
- '::1'
```
This allows you to use the standard `hideForUsers`, `showForUsers`, `hideForGroup`, and `showForGroup`. The header can not set the group. This must be done by the `auth.users` config.
---
## Alternative Authentication Methods
@ -209,17 +350,6 @@ If you are self-hosting Dashy, and require secure authentication to prevent unau
### Authentication Server
#### Authelia
[Authelia](https://www.authelia.com/) is an open-source full-featured authentication server, which can be self-hosted and either on bare metal, in a Docker container or in a Kubernetes cluster. It allows for fine-grained access control rules based on IP, path, users etc, and supports 2FA, simple password access or bypass policies for your domains.
- `git clone https://github.com/authelia/authelia.git`
- `cd authelia/examples/compose/lite`
- Modify the `users_database.yml` the default username and password is authelia
- Modify the `configuration.yml` and `docker-compose.yml` with your respective domains and secrets
- `docker-compose up -d`
For more information, see the [Authelia docs](https://www.authelia.com/docs/)
### VPN
@ -267,78 +397,6 @@ dashy.site {
}
```
### Web Server Authentication
Most web servers make password protecting certain apps very easy. Note that you should also set up HTTPS and have a valid certificate in order for this to be secure.
#### Apache
First crate a `.htaccess` file in Dashy's route directory. Specify the auth type and path to where you want to store the password file (usually the same folder). For example:
```text
AuthType Basic
AuthName "Please Sign into Dashy"
AuthUserFile /path/dashy/.htpasswd
require valid-user
```
Then create a `.htpasswd` file in the same directory. List users and their hashed passwords here, with one user on each line, and a colon between username and password (e.g. `[username]:[hashed-password]`). You will need to generate an MD5 hash of your desired password, this can be done with an [online tool](https://www.web2generators.com/apache-tools/htpasswd-generator). Your file will look something like:
```text
alicia:$apr1$jv0spemw$RzOX5/GgY69JMkgV6u16l0
```
#### NGINX
NGINX has an [authentication module](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) which can be used to add passwords to given sites, and is fairly simple to set up. Similar to above, you will need to create a `.htpasswd` file. Then just enable auth and specify the path to that file, for example:
```text
location / {
auth_basic "closed site";
auth_basic_user_file conf/htpasswd;
}
```
#### Caddy
Caddy has a [basic-auth](https://caddyserver.com/docs/caddyfile/directives/basicauth) directive, where you specify a username and hash. The password hash needs to be base-64 encoded, the [`caddy hash-password`](https://caddyserver.com/docs/command-line#caddy-hash-password) command can help with this. For example:
```text
basicauth /secret/* {
alicia JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
}
```
For more info about implementing a single sign on for all your apps with Caddy, see [this tutorial](https://joshstrange.com/securing-your-self-hosted-apps-with-single-signon/)
#### Lighttpd
You can use the [mod_auth](https://doc.lighttpd.net/lighttpd2/mod_auth.html) module to secure your site with Lighttpd. Like with Apache, you need to first create a password file listing your usernames and hashed passwords, but in Lighttpd, it's usually called `.lighttpdpassword`.
Then in your `lighttpd.conf` file (usually in the `/etc/lighttpd/` directory), load in the mod_auth module, and configure it's directives. For example:
```text
server.modules += ( "mod_auth" )
auth.debug = 2
auth.backend = "plain"
auth.backend.plain.userfile = "/home/lighttpd/.lighttpdpassword"
$HTTP["host"] == "dashy.my-domain.net" {
server.document-root = "/home/lighttpd/dashy.my-domain.net/http"
server.errorlog = "/var/log/lighttpd/dashy.my-domain.net/error.log"
accesslog.filename = "/var/log/lighttpd/dashy.my-domain.net/access.log"
auth.require = (
"/docs/" => (
"method" => "basic",
"realm" => "Password protected area",
"require" => "user=alicia"
)
)
}
```
Restart your web server for changes to take effect.
### OAuth Services
There are also authentication services, such as [Ory.sh](https://www.ory.sh/), [Okta](https://developer.okta.com/), [Auth0](https://auth0.com/), [Firebase](https://firebase.google.com/docs/auth/). Implementing one of these solutions would involve some changes to the [`Auth.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/Auth.js) file, but should be fairly straightforward.

View File

@ -36,6 +36,7 @@ The following file provides a reference of all supported configuration options.
- [`auth`](#appconfigauth-optional) - Built-in authentication setup
- [`users`](#appconfigauthusers-optional) - List or users (for simple auth)
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth config for Keycloak
- [`headerAuth`](#appconfigauthheaderauth-optional) - Auth config for HeaderAuth
- [**`sections`**](#section) - List of sections
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-sectiondisplaydatashowforkeycloakusers-itemdisplaydatahideforkeycloakusers-and-itemdisplaydatashowforkeycloakusers) - Set user controls
@ -147,6 +148,8 @@ The following file provides a reference of all supported configuration options.
**`users`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. See [`appConfig.auth.users`](#appconfigauthusers-optional). <br>**Note** this method of authentication is handled on the client side, so for security critical situations, it is recommended to use an [alternate authentication method](/docs/authentication.md#alternative-authentication-methods).
**`enableKeycloak`** | `boolean` | _Optional_ | If set to `true`, then authentication using Keycloak will be enabled. Note that you need to have an instance running, and have also configured `auth.keycloak`. Defaults to `false`
**`keycloak`** | `object` | _Optional_ | Config options to point Dashy to your Keycloak server. Requires `enableKeycloak: true`. See [`auth.keycloak`](#appconfigauthkeycloak-optional) for more info
**`enableHeaderAuth`** | `boolean` | _Optional_ | If set to `true`, then authentication using HeaderAuth will be enabled. Note that you need to have your web server/reverse proxy running, and have also configured `auth.headerAuth`. Defaults to `false`
**`headerAuth`** | `object` | _Optional_ | Config options to point Dashy to your headers for authentication. Requires `enableHeaderAuth: true`. See [`auth.headerAuth`](#appconfigauthheaderauth-optional) for more info
**`enableGuestAccess`** | `boolean` | _Optional_ | When set to `true`, an unauthenticated user will be able to access the dashboard, with read-only access, without having to login. Requires `auth.users` to be configured. Defaults to `false`.
For more info, see the **[Authentication Docs](/docs/authentication.md)**
@ -174,6 +177,15 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**[⬆️ Back to Top](#configuring)**
## `appConfig.auth.headerAuth` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`userHeader`** | `string` | _Optional_ | The Header name which contains username (default: REMOTE_USER). Case insensitive
**`proxyWhitelist`** | `array` | Required | An array of Upstream proxy servers to expect authencticated requests from
**[⬆️ Back to Top](#configuring)**
## `appConfig.webSearch` _(optional)_
**Field** | **Type** | **Required**| **Description**

View File

@ -18,7 +18,8 @@ const history = require('connect-history-api-fallback');
/* Kick of some basic checks */
require('./services/update-checker'); // Checks if there are any updates available, prints message
require('./services/config-validator'); // Include and kicks off the config file validation script
let config = {}; // setup the config
config = require('./services/config-validator'); // Include and kicks off the config file validation script
/* Include route handlers for API endpoints */
const statusCheck = require('./services/status-check'); // Used by the status check feature, uses GET
@ -27,6 +28,7 @@ const rebuild = require('./services/rebuild-app'); // A script to programmatical
const systemInfo = require('./services/system-info'); // Basic system info, for resource widget
const sslServer = require('./services/ssl-server'); // TLS-enabled web server
const corsProxy = require('./services/cors-proxy'); // Enables API requests to CORS-blocked services
const getUser = require('./services/get-user'); // Enables server side user lookup
/* Helper functions, and default config */
const printMessage = require('./services/print-message'); // Function to print welcome msg on start
@ -93,6 +95,7 @@ const app = express()
.use(ENDPOINTS.save, method('POST', (req, res) => {
try {
saveConfig(req.body, (results) => { res.end(results); });
config = req.body.config; // update the config
} catch (e) {
printWarning('Error writing config file to disk', e);
res.end(JSON.stringify({ success: false, message: e }));
@ -124,6 +127,15 @@ const app = express()
res.end(JSON.stringify({ success: false, message: e }));
}
})
// GET endpoint to return user info
.use(ENDPOINTS.getUser, (req, res) => {
try {
const user = getUser(config, req);
res.end(JSON.stringify(user));
} catch (e) {
res.end(JSON.stringify({ success: false, message: e }));
}
})
// GET fallback endpoint
.get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist', 'index.html')));

View File

@ -99,10 +99,11 @@ const printFileReadError = (e) => {
};
try { // Try to open and parse the YAML file
const config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8'));
config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8'));
validate(config);
} catch (e) { // Something went very wrong...
setIsValidVariable(false);
logToConsole(bigError());
printFileReadError(e);
}
module.exports = config;

15
services/get-user.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = (config, req) => {
try {
if ( config.appConfig.auth.enableHeaderAuth ) {
const userHeader = config.appConfig.auth.headerAuth.userHeader;
const proxyWhitelist = config.appConfig.auth.headerAuth.proxyWhitelist;
if ( proxyWhitelist.includes(req.socket.remoteAddress) ) {
return { "success": true, "user": req.headers[userHeader.toLowerCase()] };
}
}
return {};
} catch (e) {
console.warn("Error get-user: ", e);
return { 'success': false };
}
};

View File

@ -21,6 +21,7 @@ import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initial
import clickOutside from '@/directives/ClickOutside'; // Directive for closing popups, modals, etc
import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults';
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
import Keys from '@/utils/StoreMutations';
// Initialize global Vue components
@ -54,18 +55,24 @@ ErrorReporting(Vue, router);
// Render function
const render = (awesome) => awesome(Dashy);
store.dispatch(Keys.INITIALIZE_CONFIG).then((thing) => console.log('main', thing));
// Mount the app, with router, store i18n and render func
const mount = () => new Vue({
store, router, render, i18n,
}).$mount('#app');
// If Keycloak not enabled, then proceed straight to the app
if (!isKeycloakEnabled()) {
mount();
} else { // Keycloak is enabled, redirect to KC login page
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
}
store.dispatch(Keys.INITIALIZE_CONFIG).then((thing) => {
console.log('main', thing);
// Keycloak is enabled, redirect to KC login page
if (isKeycloakEnabled()) {
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else if (isHeaderAuthEnabled()) {
initHeaderAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else { // If Keycloak not enabled, then proceed straight to the app
mount();
}
});

View File

@ -450,6 +450,37 @@
}
}
},
"enableHeaderAuth": {
"title": "Enable HeaderAuth?",
"type": "boolean",
"default": false,
"description": "If set to true, enable Header Authentication. See appConfig.auth.headerAuth"
},
"headerAuth": {
"type": "object",
"description": "Configuration for headerAuth",
"additionalProperties": false,
"required": [
"proxyWhitelist"
],
"properties": {
"userHeader": {
"title": "User Header",
"type": "string",
"description": "Header name which contains username",
"default": "REMOTE_USER"
},
"proxyWhitelist": {
"title": "Upstream Proxy Auth Trust",
"type": "array",
"description": "Upstream proxy servers to expect authenticated requests from",
"items": {
"type": "string",
"description": "IPs of upstream proxies that will be trusted"
}
}
}
},
"enableKeycloak": {
"title": "Enable Keycloak?",
"type": "boolean",

77
src/utils/HeaderAuth.js Normal file
View File

@ -0,0 +1,77 @@
import axios from 'axios';
import sha256 from 'crypto-js/sha256';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
import { logout, getUserState } from '@/utils/Auth';
const getAppConfig = () => {
const Accumulator = new ConfigAccumulator();
const config = Accumulator.config();
return config.appConfig || {};
};
class HeaderAuth {
constructor() {
const { auth } = getAppConfig();
const {
userHeader, proxyWhitelist,
} = auth.headerAuth;
this.userHeader = userHeader;
this.proxyWhitelist = proxyWhitelist;
this.users = auth.users;
}
/* eslint-disable class-methods-use-this */
login() {
return new Promise((resolve, reject) => {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
axios.get(`${baseUrl}${serviceEndpoints.getUser}`).then((response) => {
if (!response.data || response.data.errorMsg) {
reject(response.data.errorMsg || 'Error');
} else {
try {
this.users.forEach((user) => {
if (user.user.toLowerCase() === response.data.user.toLowerCase()) { // User found
const strAndUpper = (input) => input.toString().toUpperCase();
const sha = strAndUpper(sha256(strAndUpper(user.user) + strAndUpper(user.hash)));
document.cookie = `${cookieKeys.AUTH_TOKEN}=${sha};`;
localStorage.setItem(localStorageKeys.USERNAME, user.user);
InfoHandler(`Succesfully signed in as ${response.data.user}`, InfoKeys.AUTH);
console.log('I think we\'re good', getUserState());
resolve(response.data.user);
}
});
} catch (e) {
reject(e);
}
}
});
});
}
logout() {
logout();
}
}
export const isHeaderAuthEnabled = () => {
const { auth } = getAppConfig();
if (!auth) return false;
return auth.enableHeaderAuth || false;
};
let headerAuth;
export const initHeaderAuth = () => {
headerAuth = new HeaderAuth();
return headerAuth.login();
};
// TODO: Find where this is implemented
export const getHeaderAuth = () => {
if (!headerAuth) {
ErrorHandler("HeaderAuth not initialized, can't get instance of class");
}
return headerAuth;
};

View File

@ -44,6 +44,7 @@ module.exports = {
rebuild: '/config-manager/rebuild',
systemInfo: '/system-info',
corsProxy: '/cors-proxy',
getUser: '/get-user',
},
/* List of built-in themes, to be displayed within the theme-switcher dropdown */
builtInThemes: [