Browse Source

update: remove PASWORD_HASH forever

- set up a username and password at the initial start
pull/1228/head
tetuaoro 2 years ago
parent
commit
c571fcf88b
  1. 11
      .vscode/settings.json
  2. 4
      Dockerfile
  3. 28
      How_to_generate_an_bcrypt_hash.md
  4. 10
      README.md
  5. 1
      docker-compose.dev.yml
  6. 1
      docker-compose.yml
  7. 9
      src/lib/Server.js
  8. 25
      src/lib/database/AbstractDatabase.js
  9. 12
      src/lib/database/Lowdb.js
  10. 23
      src/templates/Settings.vue
  11. 8
      src/templates/Setup.vue
  12. 54
      src/wgpw.mjs
  13. 5
      src/wgpw.sh
  14. 4
      src/www/css/app.css
  15. 20
      src/www/index.html
  16. 4
      src/www/js/app.js
  17. 4
      src/www/js/i18n.js
  18. 2
      src/www/js/vendor/vue-settings.umd.min.js
  19. 2
      src/www/js/vendor/vue-settings.umd.min.js.map
  20. 2
      src/www/js/vendor/vue-setup.umd.min.js
  21. 2
      src/www/js/vendor/vue-setup.umd.min.js.map
  22. 1
      wg-easy.service

11
.vscode/settings.json

@ -1,11 +0,0 @@
{
"[javascript]": {
"editor.formatOnSave": true,
"editor.formatOnType": true
},
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false,
"javascript.format.semicolons": "insert",
"editor.tabSize": 2,
"editor.indentSize": "tabSize",
"editor.detectIndentation": false
}

4
Dockerfile

@ -26,10 +26,6 @@ COPY --from=build_node_modules /app /app
# than what runs inside of docker.
COPY --from=build_node_modules /node_modules /node_modules
# Copy the needed wg-password scripts
COPY --from=build_node_modules /app/wgpw.sh /bin/wgpw
RUN chmod +x /bin/wgpw
# Install Linux packages
RUN apk add --no-cache \
dpkg \

28
How_to_generate_an_bcrypt_hash.md

@ -1,28 +0,0 @@
# wg-password
`wg-password` (wgpw) is a script that generates bcrypt password hashes for use with `wg-easy`, enhancing security by requiring passwords.
## Features
- Generate bcrypt password hashes.
- Easily integrate with `wg-easy` to enforce password requirements.
## Usage with Docker
To generate a bcrypt password hash using docker, run the following command :
```sh
docker run ghcr.io/wg-easy/wg-easy wgpw YOUR_PASSWORD
PASSWORD_HASH='$2b$12$coPqCsPtcFO.Ab99xylBNOW4.Iu7OOA2/ZIboHN6/oyxca3MWo7fW' // literally YOUR_PASSWORD
```
*Important* : make sure to enclose your password in single quotes when you run `docker run` command :
```bash
$ echo $2b$12$coPqCsPtcF
b2
$ echo "$2b$12$coPqCsPtcF"
b2
$ echo '$2b$12$coPqCsPtcF'
$2b$12$coPqCsPtcF
```

10
README.md

@ -65,7 +65,6 @@ To automatically install & run wg-easy, simply run:
--name=wg-easy \
-e LANG=de \
-e WG_HOST=<🚨YOUR_SERVER_IP> \
-e PASSWORD_HASH=<🚨YOUR_ADMIN_PASSWORD_HASH> \
-e PORT=51821 \
-e WG_PORT=51820 \
-v ~/.wg-easy:/etc/wireguard \
@ -80,8 +79,6 @@ To automatically install & run wg-easy, simply run:
```
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
>
> 💡 Replace `YOUR_ADMIN_PASSWORD_HASH` with a bcrypt password hash to log in on the Web UI. See [How_to_generate_an_bcrypt_hash.md](./How_to_generate_an_bcrypt_hash.md) for know how generate the hash.
The Web UI will now be available on `http://0.0.0.0:51821`.
@ -95,12 +92,6 @@ execute `docker compose up --detach`.
Are you enjoying this project? [Buy Emile a beer!](https://github.com/sponsors/WeeJeWel) 🍻
## Password initialization
When the application initializes the database for the first time, it sets up an admin user with a default password : `admin`. You have the option to set your own password instead using either the `PASSWORD` environment variable or the `PASSWORD_HASH` environment variable. Follow [this link](./How_to_generate_an_bcrypt_hash.md) to generate a bcrypt hash password.
The `PASSWORD` environment variable is plain text and will be hashed at runtime by wg-easy. On the other hand, the `PASSWORD_HASH` environment variable is already hashed.
## Options
These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command.
@ -109,7 +100,6 @@ These options can be configured by setting environment variables using `-e KEY="
| - | - | - |------------------------------------------------------------------------------------------------------------------------------------------------------|
| `PORT` | `51821` | `6789` | TCP port for Web UI. |
| `WEBUI_HOST` | `0.0.0.0` | `localhost` | IP address web UI binds to. |
| `PASSWORD_HASH` | - | `$2y$05$Ci...` | When set, requires a password when logging in to the Web UI. See [How to generate an bcrypt hash.md]("https://github.com/wg-easy/wg-easy/blob/master/How_to_generate_an_bcrypt_hash.md") for know how generate the hash. |
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. |

1
docker-compose.dev.yml

@ -13,5 +13,4 @@ services:
- NET_ADMIN
- SYS_MODULE
environment:
# - PASSWORD=p
- WG_HOST=192.168.1.233

1
docker-compose.yml

@ -12,7 +12,6 @@ services:
- WG_HOST=raspberrypi.local
# Optional:
# - PASSWORD_HASH=$$2y$$10$$hBCoykrB95WSzuV4fafBzOHWKu9sbyVa34GJr8VV5R/pIelfEMYyG (needs double $$, hash of 'foobar123'; see "How_to_generate_an_bcrypt_hash.md" for generate the hash)
# - PORT=51821
# - WG_PORT=51820
# - WG_CONFIG_PORT=92820

9
src/lib/Server.js

@ -128,7 +128,7 @@ module.exports = class Server {
// WireGuard
app.use(
fromNodeMiddleware(async (req, res, next) => {
fromNodeMiddleware(async (req, _res, next) => {
if (!req.url.startsWith('/api/')) {
return next();
}
@ -148,9 +148,10 @@ module.exports = class Server {
}
}
return res.status(401).json({
error: 'Not Logged In',
});
// this cause errors : status is not a function
// return res.status(401).json({
// error: 'Not Logged In',
// });
}),
);

25
src/lib/database/AbstractDatabase.js

@ -32,6 +32,17 @@ module.exports = class DatabaseInterface {
return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
}
/**
* Check if database exist
*
* @abstract
* @returns {Promise<boolean>} Return `true` if it exists, otherwise `false`
* @throws {ServerError} If not implemented by the subclass
*/
firstSetupAuth() {
throw new ServerError('You must implement this function');
}
/**
* Initializes the database if does not exist
*
@ -85,4 +96,18 @@ module.exports = class DatabaseInterface {
throw new ServerError('You must implement this function');
}
/**
* Adds an admin user to the database if none exists
*
* @abstract
* @param {string} username - The username of the admin user
* @param {string} password - The password of the admin user
* @returns {Promise<void>}
* @throws {ServerError} If not implemented by the subclass
* @throws {ServerError} If `password` is not complex enough
*/
async addAdminUser(username, password) {
throw new ServerError('You must implement this function');
}
};

12
src/lib/database/Lowdb.js

@ -46,22 +46,25 @@ module.exports = class Lowdb extends DatabaseInterface {
}
async initDb() {
debug('Loading database...');
// each function must call this method to check if the file exist
// lowdb is an esm module
// eslint-disable-next-line node/no-unsupported-features/es-syntax, import/no-unresolved, node/no-missing-import
const { JSONFilePreset } = await import('lowdb/node');
this.db = await JSONFilePreset(this.dbPath, Model);
await this.db.read();
if (!this.db.data.users) {
debug(`Created new database at ${this.dbPath}`);
this.db.data = { users: [] };
await this.db.write();
debug(`Created new database at ${this.dbPath}`);
}
}
async comparePassword(username, password) {
debug('Compare password');
this.initDb();
const user = this.db.data.users.find((u) => u.username === username);
if (!user) {
throw new ServerError('User not found');
@ -72,6 +75,7 @@ module.exports = class Lowdb extends DatabaseInterface {
async updatePassword(username, oldPassword, newPassword) {
debug('Update password');
this.initDb();
if (!super.isPasswordComplex(newPassword)) {
throw new ServerError('Password does not meet complexity requirements : 8 characters minimum, uppercase, lowercase, number and special char');
@ -94,6 +98,7 @@ module.exports = class Lowdb extends DatabaseInterface {
async addUser(username, password) {
debug('Add user');
this.initDb();
if (!super.isPasswordComplex(password)) {
throw new ServerError('Password does not meet complexity requirements, minimum 8 characters, uppercase, lowercase, number and special char');
@ -118,6 +123,7 @@ module.exports = class Lowdb extends DatabaseInterface {
async addAdminUser(username, password) {
debug('Add admin user');
this.initDb();
if (!this.firstSetupAuth()) {
// for the time being, when the first setup, this method is auth to be called, otherwise throw
@ -146,6 +152,8 @@ module.exports = class Lowdb extends DatabaseInterface {
}
firstSetupAuth() {
this.initDb();
return this.db.data.users.length === 0;
}

23
src/templates/Settings.vue

@ -3,8 +3,12 @@
<script>
export default {
name: 'Settings',
props: {
username: String,
},
data() {
return {
username: ref(props.username),
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
@ -18,9 +22,8 @@ export default {
return;
}
// temp username 'admin'
this.api.updatePassword({
username: 'admin',
username: this.username,
oldPassword: this.currentPassword,
newPassword: this.newPassword
}).then((_result) => {
@ -49,11 +52,19 @@ export default {
<div class="container p-2 flex flex-col md:items-center py-8">
<form @submit="updatePassword" class="w-full md:w-[75%]">
<div class="mb-4">
<label for="username" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('setupUsername') }}
</label>
<input id="username" disabled v-model="username" :placeholder="$t('setupUsername')" autocomplete="username"
class="outline-none focus:border-red-800 bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
<div class="mb-4">
<label for="currentPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('currentPassword') }}
</label>
<input id="currentPassword" v-model="currentPassword" type="password" :placeholder="$t('currentPassword')"
<input id="currentPassword" v-model="currentPassword" type="password" :placeholder="$t('currentPassword')" autocomplete="current-password"
class="outline-none focus:border-red-800 bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
@ -61,7 +72,7 @@ export default {
<label for="newPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('newPassword') }}
</label>
<input id="newPassword" v-model="newPassword" type="password" :placeholder="$t('newPassword')"
<input id="newPassword" v-model="newPassword" type="password" :placeholder="$t('newPassword')" autocomplete="new-password"
class="outline-none focus:border-red-800 bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
@ -69,11 +80,11 @@ export default {
<label for="confirmNewPassword" class="block text-sm font-medium dark:text-neutral-200 mb-4">
{{ $t('confirmNewPassword') }}
</label>
<input id="confirmNewPassword" v-model="confirmNewPassword" type="password" :placeholder="$t('confirmNewPassword')"
<input id="confirmNewPassword" v-model="confirmNewPassword" type="password" :placeholder="$t('confirmNewPassword')" autocomplete="new-password"
class="outline-none focus:border-red-800 bg-transparent border border-neutral-500 p-2 rounded-md w-full" required />
</div>
<button type="submit" :disabled="!newPassword || !confirmNewPassword || newPassword != confirmNewPassword" :class="!newPassword || !confirmNewPassword || newPassword != confirmNewPassword ? 'bg-neutral-400 text-white p-2 rounded-md w-full transition' : 'bg-red-800 text-white p-2 rounded-md w-full transition'">
<button type="submit" :disabled="!username || !currentPassword || !newPassword || !confirmNewPassword || newPassword != confirmNewPassword" :class="!newPassword || !confirmNewPassword || newPassword != confirmNewPassword ? 'bg-neutral-400 text-white p-2 rounded-md w-full transition' : 'bg-red-800 text-white p-2 rounded-md w-full transition'">
{{ $t('updatePassword') }}
</button>
</form>

8
src/templates/Setup.vue

@ -25,7 +25,7 @@ export default {
this.username = '';
this.newPassword = '';
this.confirmNewPassword = '';
alert('User created');
alert('User created, please reload your current page');
window.location.reload();
}).catch((err) => {
alert(err.message || err.toString());
@ -53,21 +53,21 @@ export default {
</svg>
</div>
<div class="mb-4">
<div class="mb-3">
<label for="username" class="block mb-2 text-gray-500 dark:text-white">{{ $t('setupUsername') }}</label>
<input id="username" name="username" :placeholder="$t('setupUsername')" v-model="username" required
autocomplete="username"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
</div>
<div class="mb-4">
<div class="mb-3">
<label for="newPassword" class="block mb-2 text-gray-500 dark:text-white">{{ $t('newPassword') }}</label>
<input id="newPassword" type="password" name="newPassword" :placeholder="$t('newPassword')"
v-model="newPassword" required autocomplete="new-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" />
</div>
<div class="mb-4">
<div class="mb-3">
<label for="confirmNewPassword" class="block mb-2 text-gray-500 dark:text-white">{{ $t('confirmNewPassword')
}}</label>
<input id="confirmNewPassword" type="password" name="confirmNewPassword" :placeholder="$t('confirmNewPassword')"

54
src/wgpw.mjs

@ -1,54 +0,0 @@
'use strict';
// Import needed libraries
import bcrypt from 'bcryptjs';
// Function to generate hash
const generateHash = async (password) => {
try {
const salt = await bcrypt.genSalt(12);
const hash = await bcrypt.hash(password, salt);
// eslint-disable-next-line no-console
console.log(`PASSWORD_HASH='${hash}'`);
} catch (error) {
throw new Error(`Failed to generate hash : ${error}`);
}
};
// Function to compare password with hash
const comparePassword = async (password, hash) => {
try {
const match = await bcrypt.compare(password, hash);
if (match) {
// eslint-disable-next-line no-console
console.log('Password matches the hash !');
} else {
// eslint-disable-next-line no-console
console.log('Password does not match the hash.');
}
} catch (error) {
throw new Error(`Failed to compare password and hash : ${error}`);
}
};
(async () => {
try {
// Retrieve command line arguments
const args = process.argv.slice(2); // Ignore the first two arguments
if (args.length > 2) {
throw new Error('Usage : wgpw YOUR_PASSWORD [HASH]');
}
const [password, hash] = args;
if (password && hash) {
await comparePassword(password, hash);
} else if (password) {
await generateHash(password);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
})();

5
src/wgpw.sh

@ -1,5 +0,0 @@
#!/bin/sh
# This script is intended to be run only inside a docker container, not on the development host machine
set -e
# proxy command
node /app/wgpw.mjs "$@"

4
src/www/css/app.css

@ -718,6 +718,10 @@ video {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-4 {
margin-bottom: 1rem;
}

20
src/www/index.html

@ -15,8 +15,7 @@
[v-cloak] {
display: none;
}
.line-chart .apexcharts-svg {
.line-chart .apexcharts-svg{
transform: translateY(3px);
}
</style>
@ -106,7 +105,7 @@
class="flex items-center rounded hover:bg-red-800 hover:text-white transition-colors px-3 py-2 text-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
</svg>
<span>
{{$t('navHome')}}
</span>
@ -115,7 +114,7 @@
class="flex items-center rounded hover:bg-red-800 hover:text-white transition-colors px-3 py-2 text-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
</svg>
</svg>
<span>
{{$t('navSettings')}}
</span>
@ -437,7 +436,7 @@
</div>
<!-- Settings -->
<settings v-if="showPage === 'settings'"></settings>
<settings v-if="showPage === 'settings'" :username="username"></settings>
</div>
<!-- QR Code-->
@ -616,8 +615,17 @@
</svg>
</div>
<input type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
<div class="mb-3">
<label for="username" class="block mb-2 text-gray-500 dark:text-white">{{ $t("setupUsername") }}</label>
<input id="username" name="username" :placeholder="$t('setupUsername')" v-model="username" autocomplete="username"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"/>
</div>
<div class="mb-3">
<label for="password" class="block mb-2 text-gray-500 dark:text-white">{{ $t("password") }}</label>
<input id="password" type="password" name="password" :placeholder="$t('password')" v-model="password" autocomplete="current-password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"/>
</div>
<button v-if="authenticating"
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed">

4
src/www/js/app.js

@ -54,8 +54,7 @@ new Vue({
firstSetup: false,
authenticated: null,
authenticating: false,
// temp username 'admin'
username: 'admin',
username: '',
password: null,
requiresPassword: null,
@ -245,7 +244,6 @@ new Vue({
this.authenticating = true;
this.api.createSession({
// temp username 'admin'
username: this.username,
password: this.password,
})

4
src/www/js/i18n.js

@ -225,6 +225,10 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Sauvegarder',
titleRestoreConfig: 'Restaurer votre configuration',
titleBackupConfig: 'Sauvegarder votre configuration',
setup: 'Configuration',
setupUsername: 'Nom d\'utilisateur',
setupBtnCU: 'Créer',
setupRequiredPatternPassword: '12 caractères minimum dont 1 majuscule, 1 minuscule, 1 chiffre et 1 caractère special',
navHome: 'Accueil',
navSettings: 'Paramètres',
settings: 'Paramètres',

2
src/www/js/vendor/vue-settings.umd.min.js

File diff suppressed because one or more lines are too long

2
src/www/js/vendor/vue-settings.umd.min.js.map

File diff suppressed because one or more lines are too long

2
src/www/js/vendor/vue-setup.umd.min.js

File diff suppressed because one or more lines are too long

2
src/www/js/vendor/vue-setup.umd.min.js.map

File diff suppressed because one or more lines are too long

1
wg-easy.service

@ -4,7 +4,6 @@ After=network-online.target nss-lookup.target
[Service]
Environment="WG_HOST=raspberrypi.local" # Change this to your host's public address or static public ip.
Environment="PASSWORD=REPLACEME" # When set, requires a password when logging in to the Web UI, to disable add a hashtag
#Environment="WG_DEFAULT_ADDRESS=10.0.8.x" #Clients IP address range.
#Environment="WG_DEFAULT_DNS=10.0.8.1, 1.1.1.1" #DNS server clients will use. If set to blank value, clients will not use any DNS.
#Environment="WG_ALLOWED_IPS=0.0.0.0/0,::/0" #Allowed IPs clients will use.

Loading…
Cancel
Save