-
-
Save arnehormann/9744964 to your computer and use it in GitHub Desktop.
#!/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('') | |
} |
Extension Suggestions
See https://community.openvpn.net/openvpn/wiki/Openvpn23ManPage
consider adding this to the configuration template:
ping 5
hand-window 10
Search for TODO
, MAYBE
, IDEA
and NOTE
in the source file above for some additional remarks.
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
Fun Facts
This uses a fun way to create single file service controllers - uses a nifty pattern for configuration: inject everything into a prepared class. Very flexible 😀
@@prefix
and@@parameters
followed by creation of the accessors are sufficient)AppConfig.config
fills the template at the bottom of the file.As you probably guessed,
##S:
lines are only printed for server configuration files##C:
lines are only printed for client configuration filesThe rest uses the instance variables available after creating an AppConfig with ConfigReader.