Browse Source

Do not save client private keys (#5)

* Prevent browser caching of QR codes

* Refactored restarting WG interface

* Implemented client key regen

* Added modals to notify key regen
pull/238/head
Joshua K 5 years ago
committed by joshuakraitberg
parent
commit
bba684abc2
  1. 1
      README.md
  2. 3
      docker-compose.yml
  3. 3
      src/config.js
  4. 3
      src/lib/Server.js
  5. 8
      src/lib/Util.js
  6. 34
      src/lib/WireGuard.js
  7. 153
      src/www/index.html
  8. 7
      src/www/js/api.js
  9. 25
      src/www/js/app.js

1
README.md

@ -90,6 +90,7 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L26) for the default value. |
| `WG_HARDEN_CLIENTS` | - | `1` | When clients are hardened their PrivateKeys will not be stored. All requests to obtain their config, eg. download or QR code, will trigger a key regen. |
> If you change `WG_PORT`, make sure to also change the exposed port.

3
docker-compose.yml

@ -13,7 +13,8 @@ services:
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_HARDEN_CLIENTS=1
image: weejewel/wg-easy
container_name: wg-easy
volumes:

3
src/config.js

@ -19,3 +19,6 @@ module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
module.exports.WG_POST_UP = process.env.WG_POST_UP || '';
module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || '';
module.exports.WG_DEFAULT_DNS = process.env.WG_DEFAULT_DNS;
module.exports.WG_HARDEN_CLIENTS = typeof process.env.WG_HARDEN_CLIENTS === 'string'
? process.env.WG_HARDEN_CLIENTS === '1'
: false;

3
src/lib/Server.js

@ -86,6 +86,9 @@ module.exports = class Server {
debug(`Deleted Session: ${sessionId}`);
}))
.get('/api/wireguard/hardened', Util.promisify(async req => {
return WireGuard.areClientsHardened();
}))
.get('/api/wireguard/dns', Util.promisify(async req => {
return WireGuard.getDns();
}))

8
src/lib/Util.js

@ -52,14 +52,14 @@ module.exports = class Util {
};
}
static async exec(cmd, hide=null) {
// eslint-disable-next-line no-console
static async exec(cmd, hide = null) {
if (hide == null) {
// eslint-disable-next-line no-console
console.log(`$ ${cmd}`);
} else {
// Don't log sensitive information
console.log(`$ ${cmd.replace(hide, "*HIDDEN*")}`);
// eslint-disable-next-line no-console
console.log(`$ ${cmd.replace(hide, '*HIDDEN*')}`);
}
if (process.platform !== 'linux') {

34
src/lib/WireGuard.js

@ -21,6 +21,7 @@ const {
WG_ALLOWED_IPS,
WG_POST_UP,
WG_POST_DOWN,
WG_HARDEN_CLIENTS,
} = require('../config');
module.exports = class WireGuard {
@ -126,7 +127,11 @@ AllowedIPs = ${client.address}/32`;
}
async getDns() {
return WG_DEFAULT_DNS ? WG_DEFAULT_DNS : null;
return WG_DEFAULT_DNS;
}
async areClientsHardened() {
return WG_HARDEN_CLIENTS;
}
async getClients() {
@ -201,12 +206,18 @@ AllowedIPs = ${client.address}/32`;
const config = await this.getConfig();
const client = await this.getClient({ clientId });
const privateKey = await Util.exec('wg genkey');
client.publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, privateKey);
client.preSharedKey = await Util.exec('wg genpsk');
let { privateKey } = client;
await this.saveConfig();
await this.restartGateway();
if (WG_HARDEN_CLIENTS) {
// Generate new client keys
privateKey = await Util.exec('wg genkey');
client.privateKey = null;
client.publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, privateKey);
client.preSharedKey = await Util.exec('wg genpsk');
// Restart gateway to complete key regen
await this.saveConfig();
}
return `
[Interface]
@ -240,9 +251,8 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
}
const config = await this.getConfig();
// Public key is placeholder, new one is generated on getClientConfig
const publicKey = await Util.exec('wg genpsk');
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
const preSharedKey = await Util.exec('wg genpsk');
// Calculate next IP
@ -262,14 +272,18 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
throw new Error('Maximum number of clients reached.');
}
// Avoid lint problem
const realPrivateKey = WG_HARDEN_CLIENTS ? null : privateKey;
// Create Client
const clientId = uuid.v4();
const client = {
name,
address,
realPrivateKey,
publicKey,
preSharedKey,
allowedIPs: allowedIPs,
allowedIPs,
createdAt: new Date(),
updatedAt: new Date(),

153
src/www/index.html

@ -130,7 +130,7 @@
<!-- Show QR-->
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
title="Show QR Code" @click="qrcode = `/api/wireguard/client/${client.id}/qrcode.svg?${Date.now()}`;">
title="Show QR Code" @click="clientQRShow = client; areClientsHardened().then(harden => {if (!harden) document.getElementById('client-qr-button').click();});">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -138,6 +138,16 @@
</svg>
</button>
<!-- Download Config -->
<button class="align-middle bg-gray-100 hover:bg-red-800 hover:text-white p-2 rounded transition"
title="Download Configuration" @click="clientConfigDownload = client; areClientsHardened().then(harden => {if (!harden) document.getElementById('client-download-button').click();});">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<!-- Address -->
<span class="group">
@ -367,6 +377,147 @@
</div>
</div>
<!-- Show QR code Dialog -->
<div v-if="clientQRShow" class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
-->
<div
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<!-- Heroicon name: outline/exclamation -->
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Show Client QR Code
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
Are you sure you want to display the QR code for <strong>{{clientQRShow.name}}</strong>?
This will trigger a key regeneration and invalidate existing configuration.
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button id="client-qr-button" type="button" @click="qrcode = `/api/wireguard/client/${clientQRShow.id}/qrcode.svg?${Date.now()}`; clientQRShow = null;"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
Show
</button>
<button type="button" @click="clientQRShow = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Cancel
</button>
</div>
</div>
</div>
</div>
<!-- Download client config Dialog -->
<div v-if="clientConfigDownload" class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
To: "opacity-100 translate-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 translate-y-0 sm:scale-100"
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
-->
<div
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<!-- Heroicon name: outline/exclamation -->
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Download Client Configuration
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
Are you sure you want to download the configuration for <strong>{{clientConfigDownload.name}}</strong>?
This will trigger a key regeneration and invalidate existing configuration.
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<a id="client-config-link" href="" download></a>
<button id="client-download-button" type="button" @click="var link = document.getElementById('client-config-link'); link.href = `/api/wireguard/client/${clientConfigDownload.id}/configuration`; link.click(); clientConfigDownload = null;"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
Download
</button>
<button type="button" @click="clientConfigDownload = null"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Cancel
</button>
</div>
</div>
</div>
</div>
<!-- Delete Dialog -->
<div v-if="clientDelete" class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">

7
src/www/js/api.js

@ -58,6 +58,13 @@ class API {
});
}
async areClientsHardened() {
return this.call({
method: 'get',
path: '/wireguard/hardened',
});
}
async getDns() {
return this.call({
method: 'get',

25
src/www/js/app.js

@ -38,19 +38,21 @@ new Vue({
clientsPersist: {},
clientDelete: null,
clientCreate: null,
clientQRShow: null,
clientConfigDownload: null,
clientCreateName: '',
clientCreateAllowedIPs: '',
clientCreateAllowedIPsDefault: '0.0.0.0/0, ::0/0',
clientCreateAllowedIPsExclude: (
"::/0, 1.0.0.0/8, 2.0.0.0/8, 3.0.0.0/8, "
+ "4.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, "
+ "16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, "
+ "160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, "
+ "172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, "
+ "176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, "
+ "192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, "
+ "192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, "
+ "200.0.0.0/5, 208.0.0.0/4"
'::/0, 1.0.0.0/8, 2.0.0.0/8, 3.0.0.0/8, '
+ '4.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, '
+ '16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, '
+ '160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, '
+ '172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, '
+ '176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, '
+ '192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, '
+ '192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, '
+ '200.0.0.0/5, 208.0.0.0/4'
),
clientEditName: null,
clientEditNameId: null,
@ -223,8 +225,11 @@ new Vue({
alert(err.message || err.toString());
});
},
areClientsHardened() {
return this.api.areClientsHardened();
},
getDns() {
return this.api.getDns()
return this.api.getDns();
},
createClient() {
const name = this.clientCreateName;

Loading…
Cancel
Save