Commit d3019823 authored by mehdi's avatar mehdi

Adding revocation + updating OpenVPN version + other details

parent cda97883
......@@ -3,6 +3,10 @@ MAINTAINER Mehdi Kouhen <arantes555@gmail.com>
ENV PATH /usr/local/node-6.9.5/bin:$PATH
## Adding OpenVPN repos
RUN wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
RUN echo "deb http://build.openvpn.net/debian/openvpn/stable xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list
## Installing OpenVPN, key-management tool, and iptables
RUN apt-get update -y
RUN apt-get install -y openvpn easy-rsa iptables
......
To access the key management interface, visit `/`.
/!\ Revoked keys may keep working until OpenVPN is restarted.
......@@ -38,8 +38,7 @@
</div>
</nav>
<!-- TODO: implement DELETE
<div class="modal fade" tabindex="-1" role="dialog" id="modalDelete">
<div class="modal fade" tabindex="-1" role="dialog" id="modalRevokeKey">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
......@@ -47,21 +46,23 @@
aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<h5 v-show="deleteData.isFile">Really delete <span style="font-weight: bold;">{{ deleteData.filePath }}</span>?
</h5>
<h5 v-show="deleteData.isDirectory">Really delete directory <span style="font-weight: bold;">{{ deleteData.filePath }}</span>
and all its content?</h5>
<h4 class="modal-title">
Really revoke the key for device
<span style="font-weight: bold;">{{ revokeKeyData }}</span>
?
</h4>
<h5>You won't be able to undo this action.</h5>
<div class="has-error"><div class="control-label">{{revokeKeyError}}</div></div>
</div>
<div class="modal-footer">
<button type="button" id="deleteNoBtn" class="btn btn-default" data-dismiss="modal">No</button>
<button type="button" id="deleteYesBtn" class="btn btn-danger" @click="del(deleteData)">Yes</button>
<button type="button" id="deleteNoBtn" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" id="deleteYesBtn" class="btn btn-danger" @click="revokeKey(revokeKeyData)">Revoke</button>
</div>
</div>
</div>
</div>
-->
<div class="modal fade" tabindex="-1" role="dialog" id="modalcreateKey">
<div class="modal fade" tabindex="-1" role="dialog" id="modalCreateKey">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
......@@ -71,8 +72,7 @@
<div class="modal-body">
<form @submit.prevent="createKey(createKeyData)">
<div class="form-group" :class="{ 'has-error': createKeyError }">
<input class="form-control" v-model="createKeyData" id="inputKeyName"
placeholder="Name" autofocus>
<input class="form-control" v-model="createKeyData" id="inputKeyName" placeholder="Name" autofocus>
<label class="control-label" for="inputKeyName">{{ createKeyError }}</label>
</div>
<button style="display: none;"></button>
......@@ -80,8 +80,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" @click="createKey(createKeyData)">Create
</button>
<button type="button" class="btn btn-primary" @click="createKey(createKeyData)">Create</button>
</div>
</div>
</div>
......@@ -116,18 +115,21 @@
<tbody>
<tr v-for="entry in entries">
<th><i class="fa fa-key fa-2"></i></th>
<th><a>{{ entry }}</a></th>
<th><span>--</span></th>
<th><a>{{ entry.name }}</a></th>
<th><span>{{entry.created}}</span></th>
<th style="text-align: right;">
<button class="btn btn-sm" type="button" aria-expanded="false" @click="revokeKeyAsk(entry.name)">
<i class="fa fa-trash"></i>
</button>
<div class="btn-group" data-toggle="tooltip" title="Download">
<button class="btn btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
<i class="fa fa-download"></i>&nbsp;<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><h6 class="dropdown-header no-margin">Select format</h6></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry + '?format=conf'">.conf</a></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry + '?format=ovpn'">.ovpn</a></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry + '?format=tblk'">.tblk</a></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry.name + '?format=conf'">.conf</a></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry.name + '?format=ovpn'">.ovpn</a></li>
<li><a class="dropdown-item" :href="'/api/key/' + entry.name + '?format=tblk'">.tblk</a></li>
</ul>
</div>
</th>
......
......@@ -12,9 +12,11 @@
el: '#app',
data: {
busy: true,
entries: [],
createKeyData: '',
createKeyError: null,
entries: []
revokeKeyData: '',
revokeKeyError: null
},
created () {
$('#modalcreateKey').on('shown.bs.modal', () => $('#inputKeyName').focus())
......@@ -70,13 +72,22 @@
},
*/
createKeyAsk () {
$('#modalcreateKey').modal('show')
$('#modalCreateKey').modal('show')
this.createKeyData = ''
this.createKeyError = null
},
revokeKeyAsk (name) {
$('#modalRevokeKey').modal('show')
this.revokeKeyData = name
this.revokeKeyError = null
},
createKey (name) { // TODO: validate name
this.busy = true
this.createKeyError = null
const onError = err => {
console.error('Error creating device: ', err)
this.createKeyError = 'Error creating device: ' + err
}
superagent.put('/api/key/' + this.createKeyData)
.end((error, result) => {
......@@ -87,13 +98,39 @@
this.createKeyError = 'Invalid device name'
return
}
if (result && result.statusCode !== 201) return console.error('Error creating device: ', result.statusCode)
if (error) return console.error(error)
if (result && result.statusCode !== 201) return onError(result.statusCode)
if (error) return onError(error)
this.createKeyData = ''
this.refresh()
$('#modalCreateKey').modal('hide')
})
},
revokeKey (name) { // TODO: validate name
this.busy = true
this.revokeKeyError = null
const onError = err => {
console.error('Error revoking device: ', err)
this.revokeKeyError = 'Error revoking device: ' + err
}
superagent.delete('/api/key/' + this.revokeKeyData)
.end((error, result) => {
this.busy = false
if (result && result.statusCode === 401) return this.logout()
if (result && result.statusCode === 409) {
this.revokeKeyError = 'Invalid device name'
return
}
if (result && result.statusCode !== 200) return onError(result.statusCode)
if (error) return onError(error)
this.createKeyData = ''
this.refresh()
$('#modalcreateKey').modal('hide')
$('#modalRevokeKey').modal('hide')
})
}
}
......
......@@ -12,7 +12,8 @@ cert /app/data/keys/cloudron.crt
key /app/data/keys/cloudron.key
dh /app/data/keys/dh2048.pem
tls-auth /app/data/keys/ta.key 0
cipher AES-256-GCM
crl-verify /app/data/keys/crl.pem
cipher AES-256-CBC
# Network
server 10.8.0.0 255.255.255.0
push \"redirect-gateway def1 bypass-dhcp\"
......@@ -29,5 +30,4 @@ persist-tun
verb 3
mute 20
status /run/openvpn-status.log
log-append /run/openvpn.log
"
This diff is collapsed.
......@@ -65,7 +65,7 @@ app.use('/logout', (req, res) => {
router.get('/api/list/', isAuthenticated, openvpn.list)
router.put('/api/key/*', isAuthenticated, openvpn.createKey)
router.get('/api/key/*', isAuthenticated, openvpn.getKey)
// router.delete('/api/key/*', isAuthenticated, openvpn.deleteKey)
router.delete('/api/key/*', isAuthenticated, openvpn.revokeKey)
app.use(router)
app.use('/', isAuthenticated, express.static(path.resolve(__dirname, 'app')))
app.use(lastMile())
......
......@@ -37,11 +37,12 @@ dev tun
proto tcp-client
remote ${process.env.APP_DOMAIN} ${process.env.VPN_TCP_PORT}
resolv-retry infinite
cipher AES-256-GCM
cipher AES-256-CBC
script-security 2
persist-key
persist-tun
keepalive 10 120
ns-cert-type server
# Keys
ca ca.crt
......@@ -60,7 +61,7 @@ verb 3
// env.KEY_CONFIG = childProcess.spawnSync(`${baseDir}/whichopensslcnf`, [baseDir], {env, encoding: 'utf8'}).stdout.replace(/(^\s+|\s+$)/g, '')
env.KEY_CONFIG = '/app/code/easyrsa/openssl-1.0.0.cnf'
const exec = (tag, file, args = []) => new Promise((resolve, reject) => {
const exec = ({tag, file, args, wantedCode = 0}) => new Promise((resolve, reject) => {
assert.strictEqual(typeof tag, 'string')
assert.strictEqual(typeof file, 'string')
assert(util.isArray(args))
......@@ -83,7 +84,7 @@ const exec = (tag, file, args = []) => new Promise((resolve, reject) => {
cp.on('exit', (code, signal) => {
if (errorData) console.log(`${tag} (stderr): ${errorData}`)
if (code || signal) console.log(`${tag} code: ${code}, signal: ${signal}`)
if (code === 0) return resolve()
if (code === wantedCode) return resolve()
const e = new Error(`${tag} exited with error ${code} signal ${signal}`)
e.code = code
......@@ -102,13 +103,22 @@ const cleanUserName = userName => userName.toLowerCase()
const cleanDeviceName = deviceName => deviceName.replace(/[^\w\W_0-9]+/g, '')
const readdir = (dirPath) => new Promise((resolve, reject) =>
fs.readdir(dirPath, (err, files) => err ? reject(err) : resolve(files))
const promisify = (func) => (...args) => new Promise((resolve, reject) =>
func(...args, (err, res) => err ? reject(err) : resolve(res))
)
const readdir = promisify(fs.readdir)
const stat = promisify(fs.stat)
const rm = promisify(fs.unlink)
const listUserKeys = (user) => readdir(keyDir)
.then(files => files.filter(file => file.startsWith(cleanUserName(user) + '-') && file.endsWith('.crt')))
.then(files => files.map(file => file.split('-').pop().replace(/\.crt$/, '')))
.then(files => files.filter(file => file.startsWith(cleanUserName(user) + '-') && file.endsWith('.key')))
.then(files => Promise.all(files.map(file => stat(path.join(keyDir, file))))
.then(stats => files.map((name, i) => ({
name: name.split('-').pop().replace(/\.key/, ''),
created: stats[i].birthtime
})))
)
const list = (req, res, next) => {
const user = req.user && req.user.uid ? req.user.uid : 'anonymous'
......@@ -127,12 +137,12 @@ const createKey = (req, res, next) => {
listUserKeys(user)
.then(list => {
if (list.includes(deviceName)) throw new Error('Device already exists')
return exec(
'createUserKey',
path.join(baseDir, 'pkitool'),
[`${cleanUserName(user)}-${deviceName}`]
)
if (list.map(e => e.name).includes(deviceName)) throw new Error('Device already exists')
return exec({
tag: 'createUserKey',
file: path.join(baseDir, 'pkitool'),
args: [`${cleanUserName(user)}-${deviceName}`]
})
.then(() => res.status(201).send({created: deviceName}))
})
.catch(error => next(new HttpError(500, error)))
......@@ -164,7 +174,7 @@ const getKey = (req, res, next) => {
listUserKeys(user)
.then(list => {
if (!list.includes(deviceName)) return next(new HttpError(404, 'Not Found'))
if (!list.map(e => e.name).includes(deviceName)) return next(new HttpError(404, 'Not Found'))
const certFile = `${user}-${deviceName}.crt`
const keyFile = `${user}-${deviceName}.key`
......@@ -189,10 +199,32 @@ const getKey = (req, res, next) => {
.catch(error => next(new HttpError(500, error)))
}
// TODO: add deleteKey
const revokeKey = (req, res, next) => {
const user = req.user && req.user.uid ? cleanUserName(req.user.uid) : 'anonymous'
const deviceName = decodeURIComponent(req.params[0])
if (!user) return next(new HttpError(401, 'Unidentified'))
if (!deviceName || (deviceName !== cleanDeviceName(deviceName))) return next(new HttpError(409, 'Invalid device name'))
listUserKeys(user)
.then(list => {
if (!list.map(e => e.name).includes(deviceName)) return next(new HttpError(404, 'Not Found'))
return exec({
tag: 'revokeUserKey',
file: path.join(baseDir, 'revoke-full'),
args: [`${cleanUserName(user)}-${deviceName}`],
wantedCode: 2
})
.then(() => rm(path.join(keyDir, `${cleanUserName(user)}-${deviceName}.key`)))
.then(() => res.status(200).send({revoked: deviceName}))
})
.catch(error => next(new HttpError(500, error)))
}
module.exports = {
list,
createKey,
getKey
getKey,
revokeKey
}
......@@ -33,6 +33,7 @@ if [ ! -d /app/data/keys ]; then
/app/code/easyrsa/clean-all
/app/code/easyrsa/pkitool --initca
openvpn --genkey --secret /app/data/keys/ta.key
touch /app/data/keys/crl.pem
/app/code/easyrsa/build-dh
/app/code/easyrsa/pkitool --server cloudron
fi
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment