Last active
June 21, 2022 19:29
-
-
Save arnehormann/9744964 to your computer and use it in GitHub Desktop.
Configuration file generator for OpenVPN which also sets up a ca and generates keys.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Just call this without arguments. It will show a friendly help text. | |
# For xterm-256color, it will even use colors for some commands! | |
class AppConfig | |
@@default_remote = 'vpn.example.com' | |
@@default_networks = '10.0.0.0/24>192.168.1.0/24' | |
@@default_subject = '/C=US/ST=CA/L=San Francisco/O=Example/OU=/CN={{name}}' | |
# If you want to use eliptic curve crypto, change the code in Crypto below. | |
# As is, it only uses RSA, AES in CBC mode and SHA | |
# lookup with: openvpn --show-ciphers | |
@@default_cipher = 'AES-256-CBC' | |
# lookup with: openvpn --show-tls | |
@@default_tls_ciphers = %w( | |
TLS-DHE-RSA-WITH-AES-256-GCM-SHA384 | |
TLS-DHE-RSA-WITH-AES-256-CBC-SHA256 | |
TLS-DHE-RSA-WITH-AES-128-GCM-SHA256 | |
TLS-DHE-RSA-WITH-AES-128-CBC-SHA256 | |
TLS-DHE-RSA-WITH-AES-256-CBC-SHA | |
TLS-DHE-RSA-WITH-AES-128-CBC-SHA | |
).join(':') | |
### REQUIRED CLASS VARIABLES | |
# environment variable prefix for this app | |
@@prefix = 'VPN_' | |
# [field, default, description] | |
# Cases for default: | |
# nil - not set, optional | |
# empty Array - not set, mandatory | |
# Symbol - default value of another field | |
# String, Fixnum - that value | |
@@parameters = [ | |
# values mainly used in config file generation | |
[:devName, 'tun', 'name of the server\'s tunnel device'], | |
[:devNode, nil, 'name of the device node for Windows TUN/TAP driver'], | |
[:protocol, 'udp', 'openvpn protocol'], | |
[:port, 1194, 'external openvpn port on server'], | |
[:portIntra, :port, 'internal openvpn port on server'], | |
[:serverDev, 'eth0', 'server network device name'], | |
[:user, 'openvpn', 'openvpn user for privilege separation'], | |
[:group, :user, 'openvpn group for privilege separation'], | |
[:remote, @@default_remote, 'reachable address of vpn server'], | |
[:networks, @@default_networks, 'from network-ip/range to network-ip/range'], | |
[:cipher, @@default_cipher, 'used encryption cipher'], | |
[:tlsCipher, @@default_tls_ciphers, 'encryption ciphers used for tls'], | |
[:maxClients, 16, 'maximum number of simultaneous clients (server)'], | |
# values used in creation of certificates | |
[:bits, 2048, 'bits per key or parameter'], | |
[:bitsDh, :bits, 'diffie hellman parameter bits'], | |
[:bitsCa, :bits, 'certificate authority rsa key bits'], | |
[:bitsServer, :bits, 'server rsa key bits'], | |
[:bitsClient, :bits, 'client rsa key bits'], | |
[:certSubject, @@default_subject, 'certificate subject, "{{name}}" can use argument'], | |
[:certValid, 3650, 'validity of certificate in days'], | |
[:certValidCa, :certValid, 'validity of ca certificate in days'], | |
[:fileSerial, nil, 'file to add serial and subject to (needed for crl)'] | |
] | |
# all options defined above are injected with ConfigReader | |
# but are read-only | |
attr_reader *(@@parameters.collect{|a| a[0]}) | |
# unparsed arguments | |
attr_reader :unparsed | |
attr_reader :logVerbosity, | |
:fromNet, | |
:toNet | |
attr_reader :fileTa, | |
:fileDh, | |
:fileCrl, | |
:fileCaKey, | |
:fileCaCert, | |
:fileKey, | |
:fileCert | |
attr_accessor :mode, :name | |
attr_accessor :blobTa, | |
:blobDh, | |
:blobCa, | |
:blobServer, | |
:blobKey, | |
:blobCert | |
def build() | |
# fill in synthetic values | |
@logVerbosity = 3 | |
@fromNet, @toNet = @networks.split('>') | |
@fileTa = 'ta.key' | |
@fileDh = "dh#{@bitsDh}.pem" | |
@fileCrl = 'crl.pem' | |
@fileCaKey = 'ca.key' | |
@fileCaCert = 'ca.pem' | |
return self | |
end | |
def use() | |
# @mode is set after the call to build() | |
@fileKey = "#{@name || @mode}.key" | |
@fileCert = "#{@name || @name}.pem" | |
return self | |
end | |
def server? | |
return @mode == 'server' | |
end | |
def net_format(what, format) | |
return case format | |
when :first | |
what.sub(/\.[^\.]*$/, '.1/32') | |
when :netmask | |
ip, range = what.split('/') | |
nm = 0xffffffff ^ (0xffffffff >> range.to_i) | |
netmask = (0..3).collect{ |i| ((nm >> (3-i)*8) & 0xff) }.join('.') | |
"#{ip} #{netmask}" | |
else | |
what | |
end | |
end | |
def linkPem(pemType) | |
key, file, blob = { | |
ta: ['tls-auth', @fileTa, @blobTa], | |
dh: ['dh', @fileDh, @blobDh], | |
ca: ['ca', @fileCaCert, @blobCa], | |
cert: ['cert', @fileCert, @blobCert], | |
key: ['key', @fileKey, @blobKey] | |
}[pemType] | |
if blob | |
blob = blob.gsub(/^[^a-zA-Z0-9=+\/\-].*/, '') | |
blob.strip!() | |
"<#{key}>\n#{blob}\n</#{key}>\n" | |
elsif file | |
"#{key} #{file}\n" | |
else | |
"" | |
end | |
end | |
def config() | |
# everything after __END__ marker is the template | |
template = DATA.read | |
if server? | |
# is in server mode, remove server annotations | |
template.gsub!(/##S:[ \t]*/, '') | |
else | |
# is in client mode, remove client annotations | |
template.gsub!(/##C:[ \t]*/, '') | |
end | |
# remove remaining annotations including their content | |
template.gsub!(/##[SC]:[^\n]*\n?/, '') | |
# evaluate the template in context of this instance | |
out = instance_eval("<<CONFIG_END\n" + template + "\nCONFIG_END") | |
# fix whitespace: none at the front, just \n at the end, no more than | |
# one blank line inside | |
out.strip! | |
out.gsub!(/([\t ]*\n){3,}/, "\n\n") | |
return out + "\n" | |
end | |
end | |
module ConfigReader | |
# ARGV as a map including all parameters in the form | |
# -KEY=VALUE | |
# and the rest in an Array with the key :unparsed | |
@@argMap = ARGV.inject({unparsed: []}) do |a, v| | |
begin | |
key, val = v.scan(/^(-[^=]*)=?(.*)$/)[0] | |
a[key] = val.empty? ? true : val | |
rescue | |
a[:unparsed] << v | |
end | |
a | |
end | |
# read order for configuration parameters. | |
# Per default: | |
# 1. lookup in ARGV (command line arguments) | |
# 2. lookup in ENV (environment variables) | |
# 3. use default value | |
@@readOrder = [ | |
[:arg, @@argMap], | |
[:env, ENV], | |
:default | |
] | |
# take the array of usable parameters in the format | |
# [[NAME:symbol, DEFAULT, DESCRIPTION], ...] | |
# and the environment variable prefix and | |
# return a map {NAME => CONFIGURATION_PARAMETER, ...} | |
# where CONFIGURATION_PARAMETER is | |
# { | |
# symbol: NAME, | |
# arg: COMMAND_LINE_VARIABLE_PREFIX, | |
# env: ENVIRONMENT_VARIABLE_NAME, | |
# default: DEFAULT_VALUE, | |
# desc: DESCRIPTION | |
# } | |
def self.seed(parameters, envPrefix) | |
envPrefix ||= '' | |
data = {} | |
parameters.each do |p| | |
symbol, defval, description = p | |
# key in command line arguments: | |
# capital letters in symbol are replaced by '-' and lower case | |
key = symbol.to_s().gsub(/([A-Z])/, '-\1').downcase() | |
argkey = '-' + key | |
# key in environment variables: | |
# add envPrefix in front and use upper casse version of | |
# command line key with '-' replaced with '_' | |
envkey = envPrefix + key.gsub('-', '_').upcase() | |
data[symbol] = { | |
symbol: symbol, | |
arg: argkey, | |
env: envkey, | |
default: defval, | |
desc: description | |
} | |
end | |
data | |
end | |
# fetch a configuration parameter with its description from seed. | |
# returns [value, is_set] | |
def self.value(parameter) | |
@@readOrder.each do |source| | |
if source == :default | |
return [parameter[:default], false] | |
end | |
pk, values = source | |
key = parameter[pk] | |
if values.has_key? key | |
return [values[key], true] | |
end | |
end | |
end | |
def self.fill(config) | |
missing = [] | |
config.each do |key, cfgparam| | |
v, s = value(cfgparam) | |
if s || v.class != Symbol | |
cfgparam[:value] = v | |
cfgparam[:is_set] = s | |
else | |
missing << [key, v] | |
end | |
end | |
until missing.empty? | |
still_missing = [] | |
missing.each do |link| | |
key, ref = link | |
reference = config[ref] | |
if reference.has_key? :value | |
cfgparam = config[key] | |
cfgparam[:value] = reference[:value] | |
cfgparam[:is_set] = false | |
else | |
still_missing.push([key, ref]) | |
end | |
end | |
if missing.length == still_missing.length | |
raise Exception.new("unpaired config defaults for #{missing.inspect}") | |
end | |
missing = still_missing | |
end | |
return config | |
end | |
def self.create(cfgClass) | |
envPrefix = cfgClass.class_variable_get(:@@prefix) | |
parameters = cfgClass.class_variable_get(:@@parameters) | |
config = seed(parameters, envPrefix) | |
cfgClass.class_variable_set(:@@expandedParameters, config) | |
instance = cfgClass.new | |
instance.instance_variable_set("@unparsed", @@argMap[:unparsed]) | |
fill({}.merge(config)).each do |k, v| | |
instance.instance_variable_set("@#{k}", v[:value]) | |
end | |
instance.build() | |
end | |
end | |
module CliHelp | |
def self.colorizer() | |
if ENV['TERM'] == 'xterm-256color' | |
# See http://stackoverflow.com/questions/1489183/colorized-ruby-output | |
# and http://ascii-table.com/ansi-escape-sequences.php | |
return Proc.new do |str, col| | |
case col | |
when :key # bold gray | |
"\033[37;1m#{str}\033[0m" | |
when :argkey # bold gray, bg-cyan | |
"\033[46;37;1m#{str}\033[0m" | |
when :envkey # bold gray, bg-blue | |
"\033[44;37;1m#{str}\033[0m" | |
when :value # bold brown | |
"\033[33;1m#{str}\033[0m" | |
when :default # brown | |
"\033[33m#{str}\033[0m" | |
when :warn # red | |
"\033[31m#{str}\033[0m" | |
else | |
str.to_s() | |
end | |
end | |
end | |
return Proc.new do |str, col| | |
str.to_s() | |
end | |
end | |
def self.help(app) | |
color = colorizer() | |
lines = [] | |
cfg = app.class.class_variable_get(:@@expandedParameters) | |
cfg.each do |k, parameter| | |
value = app.send(k) | |
argkey = parameter[:arg] | |
envkey = parameter[:env] | |
defval = parameter[:default] | |
desc = parameter[:desc] | |
dvtxt = case defval | |
when String | |
': "' + color[defval, :default] + '"' | |
when Symbol | |
" is the value of #{color[cfg[defval][:arg], :argkey]}" | |
else | |
if defval == nil | |
" does not exist" | |
else | |
": #{color[defval, :default]}" | |
end | |
end | |
printval = case | |
when !parameter[:is_set] | |
if defval == [] | |
color["not set", :warn] + ',' | |
else | |
'not set,' | |
end | |
when value == defval | |
'the' | |
else | |
"\"#{color[value, :value]}\"," | |
end | |
lines << [ | |
"#{color[argkey, :argkey]} / #{color[envkey, :envkey]}", | |
" #{desc};", | |
" Is #{printval} default value#{dvtxt}" | |
].join("\n") | |
end | |
return ( | |
[ | |
"Configuration parameters as #{color['argument', :argkey]} " + | |
"/ #{color['environment', :envkey]} variables" | |
] + lines.sort() + | |
[ | |
"unknown arguments:", | |
app.unparsed | |
] | |
).join("\n") | |
end | |
def self.example(app, all) | |
cfg = app.class.class_variable_get(:@@expandedParameters) | |
vars = cfg.keys.sort.inject('') do |a, k| | |
arg = cfg[k] | |
value = app.send(k) | |
if all || arg[:is_set] && value != arg[:default] | |
a += "#{arg[:env]}='#{value}' " | |
end | |
a | |
end | |
return vars + __FILE__ + ' ...' | |
end | |
end | |
module Crypto | |
require 'openssl' | |
@@DIGEST = OpenSSL::Digest::SHA256 | |
@@SERIAL_DIGEST = @@DIGEST | |
@@SIGN_DIGEST = @@DIGEST | |
def self.serial(*certs) | |
hash = @@SERIAL_DIGEST.new | |
certs.each { |cert| hash << cert } | |
# serial number binary length is max. 20 bytes: | |
# use last 20 bytes from hash | |
return OpenSSL::BN.new(hash.digest[-20, 20], 2) | |
end | |
def self.asName(value, defaultValue) | |
return case value | |
when NilClass | |
defaultValue | |
when String | |
OpenSSL::X509::Name.parse(value) | |
when OpenSSL::X509::Name | |
value | |
when OpenSSL::X509::Certificate | |
value.subject | |
when Array | |
OpenSSL::X509::Name.new(value) | |
else | |
raise Exception.new("asName: could not [#{value}], unhandled class #{value.class}") | |
end | |
end | |
def self.getKey(filename, pkClass, *generate_args) | |
if filename != nil && (File.exists? filename) | |
return pkClass.new(File.read(filename)) | |
end | |
return pkClass.generate(*generate_args) | |
end | |
def self.storePem(filename, pemable) | |
return if File.exists? filename | |
mode = 0644 | |
if pemable.respond_to?(:private?) && pemable.private? | |
mode = 0600 | |
end | |
File.open(filename, 'w', mode) do |file| | |
file.write(pemable.to_pem) | |
end | |
end | |
def self.addExt(issuer, extable, extensions) | |
factory = OpenSSL::X509::ExtensionFactory | |
ef = case extable | |
when OpenSSL::X509::Certificate | |
factory.new(issuer, extable) | |
when OpenSSL::X509::Request | |
factory.new(issuer, nil, extable) | |
when OpenSSL::X509::CRL | |
factory.new(issuer, nil, nil, extable) | |
else | |
raise Exception.new("type #{extable.class} can handle extensions") | |
end | |
extensions.each do |k, v| | |
extable.add_extension(ef.create_extension(k, v)) | |
end | |
return extable | |
end | |
def self.newCert(info) | |
cert = OpenSSL::X509::Certificate.new() | |
cert.version = 2 | |
serial = info[:serial] | |
cert.serial = case serial | |
when NilClass | |
1 | |
when Fixnum, OpenSSL::BN | |
serial | |
when Array | |
serial(*serial) | |
else | |
raise Exception.new("invalid serial [#{info[:serial]}]") | |
end | |
subject = asName(info[:subject], nil) | |
if subject == nil | |
raise Exception.new("invalid subject [#{info[:subject]}]") | |
end | |
cert.subject = subject | |
cert.issuer = asName(info[:issuerCert], subject) | |
key = info[:key] | |
cert.public_key = key.public_key | |
cert.not_before = info[:starts] | |
cert.not_after = info[:expires] | |
cert = addExt(info[:issuerCert] || cert, cert, info[:extension]) | |
cert.sign(info[:signKey] || key, @@SIGN_DIGEST.new) | |
end | |
def self.loadAs(filename, cls) | |
return nil unless filename != nil && (File.exists? filename) | |
cert = File.read(filename) | |
return cls.new(cert) | |
end | |
## | |
## general use functions, the ones above are rough and internal | |
## | |
def self.rsaPrivKey(filename, bits) | |
# use public exponent of 3 (RSA_3) instead of 65537 (RSA_F4) | |
# to speed up operations. Claims of security improvements by | |
# using larger exponents are appearently misguided: | |
# https://www.imperialviolet.org/2012/03/16/rsae.html (Three should be fine) | |
getKey(filename, OpenSSL::PKey::RSA, bits, 3) | |
end | |
def self.loadCert(filename) | |
return self.loadAs(filename, OpenSSL::X509::Certificate) | |
end | |
def self.loadRSAKey(filename) | |
return self.loadAs(filename, OpenSSL::PKey::RSA) | |
end | |
def self.ta(app) | |
return File.read(app.fileTa) if File.exists? app.fileTa | |
# The code below is an alternative for | |
# `openvpn --genkey --secret #{app.fileTa}` | |
# but returns the ta key pem as a String | |
header, footer = ['BEGIN', 'END'].collect do |s| | |
"-----#{s} OpenVPN Static key V1-----" | |
end | |
num_bytes = 4 * 64 # 2 keys with 64 bytes for hmac and cipher | |
body = OpenSSL::Random. | |
random_bytes(num_bytes). | |
unpack("h32" * (num_bytes / 16)) | |
return ([header] + body + [footer, '']).join("\n") | |
end | |
def self.dh(app) | |
# use default generator 2 , but make it explicit | |
getKey(app.fileDh, OpenSSL::PKey::DH, app.bitsDh, 2) | |
end | |
def self.ca(app, key) | |
cert = loadCert(app.fileCaCert) | |
if cert | |
return cert | |
end | |
starts = Time.now | |
expires = starts + app.certValidCa.to_i * 24 * 60 * 60 | |
subject = app.certSubject.gsub('{{name}}', app.name || app.mode) | |
return newCert({ | |
key: key, | |
subject: subject, | |
starts: starts, | |
expires: expires, | |
extension: { | |
'basicConstraints' => 'CA:TRUE,pathlen:1', | |
'subjectKeyIdentifier' => 'hash', | |
# NOTE: | |
# Removing issuer from authorityKeyIdentifier would | |
# allow for certificate regeneration from the same | |
# key which keeps all signed certificates valid. | |
# As is, everything is invalidated when the CA | |
# certificate expires. | |
'authorityKeyIdentifier' => 'keyid,issuer:always', | |
'keyUsage' => 'cRLSign,keyCertSign' | |
} | |
}) | |
end | |
def self.cert(app, key, caKey, caCert) | |
# NOTE: | |
# For a CRL, the required info can be extracted from the | |
# configuration by piping the configuration file input with | |
# its embedded certificate into | |
# awk 'BEGIN{PRINT=0}{if($0=="<cert>"){PRINT=1}else if($0=="</cert>"){PRINT=0}else if(PRINT==1){print $0}}'\ | |
# | openssl x509 -serial -subject -noout | |
cert = loadCert(app.fileCert) | |
if cert | |
return cert | |
end | |
starts = Time.now | |
expires = starts + app.certValidCa.to_i * 24 * 60 * 60 | |
subject = app.certSubject.gsub('{{name}}', app.name || app.mode) | |
ext = { | |
'basicConstraints' => 'CA:FALSE', | |
'subjectKeyIdentifier' => 'hash', | |
'authorityKeyIdentifier' => 'keyid,issuer:always', | |
} | |
if app.server? | |
ext = ext.merge({ | |
'extendedKeyUsage' => 'serverAuth', | |
'keyUsage' => 'digitalSignature,keyEncipherment' | |
}) | |
else | |
ext = ext.merge({ | |
'extendedKeyUsage' => 'clientAuth', | |
'keyUsage' => 'digitalSignature' | |
}) | |
end | |
return newCert({ | |
key: key, | |
subject: subject, | |
issuerCert: caCert, | |
starts: starts, | |
expires: expires, | |
signKey: caKey, | |
serial: [caCert.to_pem(), key.public_key.to_pem()], | |
extension: ext | |
}) | |
end | |
def self.crl(lastCrl, caKey, caCert, *revokeSerials) | |
crl = case lastCrl | |
when OpenSSL::X509::CRL | |
lastCrl | |
when String, File | |
OpenSSL::X509::CRL.new(lastCrl) | |
when NilClass | |
new_crl = OpenSSL::X509::CRL.new | |
new_crl.issuer = caCert.subject | |
new_crl.version = 0 | |
cert = addExt(caCert, new_crl, { | |
'authorityKeyIdentifier' => 'keyid:always,issuer:always' | |
}) | |
new_crl | |
else | |
raise Exception.new("invalid CRL") | |
end | |
now = Time.now | |
crl.last_update = now | |
crl.next_update = now + 60 * 60 # in one hour | |
crl.version += 1 | |
revokeSerials.each do |serial| | |
rev = OpenSSL::X509::Revoked.new | |
rev.time = now | |
rev.serial = case serial | |
when OpenSSL::BN | |
serial | |
when String | |
OpenSSL::BN.new(serial) | |
else | |
raise Exception.new("unsupported serial type #{serial.class} for '#{serial}'") | |
end | |
crl.add_revoked rev | |
end | |
crl.sign(caKey, @@SIGN_DIGEST.new) | |
return crl | |
end | |
# TODO still missing encryption of keys | |
# IDEA generate template of client string to get rid of ta.key and ca files | |
end | |
# TODO handling of key en-/decryption passwords | |
# TODO certificate generation with external private key - public key as argument instead | |
if __FILE__ == $0 | |
app = ConfigReader.create(AppConfig) | |
mode = app.unparsed.shift | |
case mode | |
when 'parameters' | |
puts CliHelp.help(app.use()) | |
when 'config', 'config-full' | |
puts CliHelp.example(app.use(), mode == 'config-full') | |
when 'setup' | |
app.mode = mode | |
app.name = app.unparsed[0] || 'CA' | |
app.use() | |
caKey = Crypto.rsaPrivKey(app.fileCaKey, app.bitsCa) | |
Crypto.storePem(app.fileCaKey, caKey) | |
cert = Crypto.ca(app, caKey) | |
Crypto.storePem(app.fileCaCert, cert) | |
crl = Crypto.crl(nil, caKey, cert) | |
Crypto.storePem(app.fileCrl, crl) | |
when 'server', 'client' | |
app.mode = mode | |
app.name = app.unparsed[0] || mode | |
app.use() | |
if app.server? | |
app.blobTa = Crypto.ta(app) | |
unless File.exists?(app.fileTa) | |
File.open(app.fileTa, 'w', 0644) do |file| | |
file.write(app.blobTa) | |
end | |
end | |
# dh parameters are server only | |
app.blobDh = Crypto.dh(app).to_pem() | |
end | |
app.blobTa ||= File.read(app.fileTa) | |
caKey = Crypto.loadRSAKey(app.fileCaKey) | |
caCert = Crypto.loadCert(app.fileCaCert) | |
app.blobCa ||= caCert.to_pem() | |
key = Crypto.rsaPrivKey( | |
nil, # do not store on HDD | |
app.server? ? app.bitsServer : app.bitsClient | |
) | |
app.blobKey = key.to_pem() | |
cert = Crypto.cert(app, key, caKey, caCert) | |
if app.fileSerial | |
File.open(app.fileSerial, 'a', 0600) do |file| | |
values = [:serial, :not_before, :subject].collect{|f| cert.send(f)} | |
file.puts values.join("\t") | |
end | |
end | |
app.blobCert = cert.to_pem() | |
puts app.config() | |
when 'revoke' | |
puts Crypto.crl( | |
File.read(app.fileCrl), | |
Crypto.loadRSAKey(app.fileCaKey), | |
Crypto.loadCert(app.fileCaCert), | |
*app.unparsed | |
).to_pem | |
else | |
program = __FILE__ | |
puts <<HELP_END | |
#{program} sets up and manages OpenVPN. | |
It can generate a new certificate authority and create client and server | |
configuration. To run OpenVPN, please read the configuration files, the | |
comment at the beginning of the server configuration file provides | |
the instructions. | |
How to call #{program} (best start with parameters and config-full): | |
$ #{program} help this help text | |
$ #{program} parameters lists available configuration parameters | |
$ #{program} config-full example for a call with full current configuration | |
$ #{program} config like config-full without defaults | |
$ #{program} setup [CANAME] create a CA and a CRL | |
$ #{program} server [SRVNAME] print a server config file | |
$ #{program} client NAME print a client config file | |
$ #{program} revoke SERIAL* print crl with added revoked serials | |
HELP_END | |
end | |
end | |
# TODO: reduce "hand-window" | |
__END__ | |
##S: # Per installation, run these on the server (assuming Linux): | |
##S: # $: useradd -M '#{@user}' | |
##S: # Also run these and consider adding them to /etc/rc.local so they are preserved on reboot | |
##S: # $: sysctl -w net.ipv4.ip_forward=1 | |
##S: # $: ip link set dev '#{@serverDev}' promisc on | |
##S: # $: iptables -A FORWARD -i '#{@serverDev}' -o '#{@devName == 'tun' ? 'tun0' : @devName}' -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT | |
##S: # $: iptables -A FORWARD -s '#{@fromNet}' -o '#{@serverDev}' -j ACCEPT | |
##S: # $: iptables -t nat -A POSTROUTING -s '#{@fromNet}' '!' -d '#{net_format(@fromNet, :first)}' -o '#{@serverDev}' -j MASQUERADE | |
##S: | |
##S: # For a management console: | |
##S: # connect with | |
##S: # telnet 127.0.0.1 20000 | |
##S: # and uncomment below to enable | |
##S: # management 127.0.0.1 20000 | |
##C: client | |
##S: server #{net_format(@fromNet, :netmask)} | |
proto udp | |
##C: remote #{@remote} #{@port} | |
##S: port #{@portIntra} | |
##S: push "route #{net_format(@toNet, :netmask)}" | |
dev-type tun | |
dev #{@devName || 'tun'} | |
#{@devNode ? "dev-node #{@devNode}" : ""} | |
# remove this if there are Windows clients with OpenVPN < 2.1 | |
topology subnet | |
comp-lzo adaptive | |
cipher #{@cipher} | |
##S: # see https://community.openvpn.net/openvpn/wiki/Hardening | |
##S: tls-cipher #{@tlsCipher} | |
##S: remote-cert-tls client | |
##C: remote-cert-tls server | |
##S: | |
##S: persist-key | |
##S: persist-tun | |
##S: persist-local-ip | |
##S: persist-remote-ip | |
##S: push "persist-key" | |
##S: push "persist-tun" | |
##C: | |
##C: nobind | |
##S: max-clients #{@maxClients || 32} | |
##S: keepalive 4 20 | |
##S: | |
##S: #{@user ? "user #{@user}" : ''} | |
##S: #{@group ? "group #{@group}" : ''} | |
verb #{@logVerbosity || 3} | |
##S: mute 5 | |
##S: # status logging, ip pool persist | |
##S: ifconfig-pool-persist ipp.txt | |
##S: status openvpn-status.log | |
##S: #{@fileCrl ? "crl-verify #{@fileCrl}" : ''} | |
key-direction #{server? ? '0' : '1'} | |
#{ | |
((server? ? [:dh] : []) + [:ta, :ca, :cert, :key]). | |
collect{ |pem| linkPem(pem) }.join('') | |
} |
Troubleshooting
This script somehow fails when it's called from Windows. I didn't have Windows on hand myself and couldn't debug it. The configurations work, though.
When OpenVPN is installed on a Raspberry Pi with Raspbian, the tls cipher list is borked.
First, update OpenSSL so you are heartbleed safe: openssl version -a
should show a build date of April 20th 2014 or more recent.
Call openvpn --show-tls
and copy the list.
Remove all entries containing RC4, DES, 3DES, NULL and ECDH.
Proofread what's left, I don't know what OpenVPN can handle in which version on which hardware.
Put the rest into your configuration:
- ":" separated in the server configuration:
tls-cipher
- in the generator:
AppConfig.@@default_tls_ciphers
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Extension Suggestions
See https://community.openvpn.net/openvpn/wiki/Openvpn23ManPage
consider adding this to the configuration template:
Search for
TODO
,MAYBE
,IDEA
andNOTE
in the source file above for some additional remarks.