0f974c07494b1a0f8538411049705f10ecd6a716
[vpn-setup] / vpn-mk-client-cert
1 #!/bin/bash
2 # Copyright (C) 2016 Ian Kelling
3
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7
8 # http://www.apache.org/licenses/LICENSE-2.0
9
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 set -eE -o pipefail
17 trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" returned $?" >&2' ERR
18
19 [[ $EUID == 0 ]] || exec sudo -E "$BASH_SOURCE" "$@"
20
21 readonly this_file="$(readlink -f -- "${BASH_SOURCE[0]}")"
22 this_dir="${this_file%/*}"
23
24
25 usage() {
26 cat <<'EOF'
27 usage: ${0##*/} VPN_SERVER_HOST
28
29 -b COMMON_NAME By default, use $CLIENT_HOST or if it is not given,
30 $HOSTNAME. If the cert already exists on the server,
31 with the CLIENT_NAME name, we use the existing one. See
32 comment below if we ever want to check existing common
33 names. They must be unique per server, so you can use
34 $(uuidgen) if needed. You used to be able to create
35 multiple with the same name, but not connect at the
36 same time, but now, the generator keeps track, so you
37 can't generate.
38
39 -c CLIENT_HOST Default is localhost. Else we ssh to root@CLIENT_HOST.
40 -f Force. Proceed even if cert already exists.
41 -n CONFIG_NAME default is client
42 -o SERVER_CONFIG_NAME Default is CONFIG_NAME
43 -r Install certs to the current directory instead of /etc/openvpn/client
44 -s SCRIPT_PATH Use custom up/down script at SCRIPT_PATH. If client host is
45 not localhost, the script is copied to it. The default
46 script used to be /etc/openvpn/update-resolv-conf, but now
47 that systemd-resolved is becoming popular, there is no default.
48
49 Environment variable: SSH_CONFIG_FILE_OVERRIDE
50
51 Generate a client cert and config and install it on locally or on
52 CLIENT_HOST if given. Uses default config options, and expects be able
53 to ssh to VPN_SERVER_HOST and CLIENT_HOST as root, or if CLIENT_HOST is
54 localhost, just to sudo this script as root.
55
56
57
58
59 Note: Uses GNU getopt options parsing style
60 EOF
61 exit ${1:-0}
62 }
63
64 # to get the common name
65 # cn=$(s openssl x509 -noout -nameopt multiline -subject \
66 # -in /etc/openvpn/client/mail.crt | \
67 # sed -rn 's/^\s*commonName\s*=\s*(.*)/\1/p')
68
69
70 ####### begin command line parsing and checking ##############
71
72 shell="bash -c"
73 name=client
74 client_host=$CLIENT_HOST
75 force=false
76 rel=false
77 if [[ $SSH_CONFIG_FILE_OVERRIDE ]]; then
78 ssh_arg="-F $SSH_CONFIG_FILE_OVERRIDE"
79 fi
80
81 temp=$(getopt -l help hb:c:fn:o:rs: "$@") || usage 1
82 eval set -- "$temp"
83 while true; do
84 case $1 in
85 -b) common_name="$2"; shift 2 ;;
86 -c) client_host=$2; shell="ssh $ssh_arg root@$client_host"; shift 2 ;;
87 -f) force=true; shift ;;
88 -n) name="$2"; shift 2 ;;
89 -o) server_name="$2"; shift 2 ;;
90 -r) rel=true; shift ;;
91 -s) script="$2"; shift 2 ;;
92 -h|--help) usage ;;
93 --) shift; break ;;
94 *) echo "$0: Internal error! unexpected args: $*" ; exit 1 ;;
95 esac
96 done
97
98 if [[ $client_host ]] && $rel; then
99 echo "$0: error client_host and -r specified. use one or the other"
100 exit 1
101 fi
102
103
104 if [[ ! $server_name ]]; then
105 server_name="$name"
106 fi
107
108 if [[ ! $common_name ]]; then
109 if [[ $client_host ]]; then
110 common_name=$client_host
111 else
112 common_name=$HOSTNAME
113 fi
114 fi
115
116 host=$1
117 [[ $host ]] || usage 1
118
119 ####### end command line parsing and checking ##############
120
121
122 if $rel; then
123 f=$name.crt
124 keydir=.
125 else
126 f=/etc/openvpn/client/$name.crt
127 keydir=/etc/openvpn/client
128 fi
129
130
131 if ! $force; then
132 cert_to_test=$f
133 if [[ $client_host ]]; then
134 cert_to_test=$(mktemp)
135 ssh $ssh_arg root@$client_host cat $f 2>/dev/null >$cert_to_test ||:
136 fi
137 if openssl x509 -checkend $(( 60 * 60 * 24 * 30 )) -noout -in $cert_to_test &>/dev/null; then
138 if [[ $client_host ]]; then
139 prefix="$shell"
140 fi
141 if $prefix test -s $keydir/ta-$name.key -a -s $keydir/ca-$name.crt; then
142 echo "$0: cert already exists. exiting early"
143 exit 0
144 fi
145 fi
146 fi
147
148 port=$(echo '/^port/ {print $2}' | ssh $ssh_arg root@$host awk -f - /etc/openvpn/server/$name.conf | tail -n1)
149
150 $shell "dd of=$keydir/$name.conf" <<EOF
151 # From example config, from debian stretch to buster
152 client
153 dev tun
154 proto udp
155 remote $host $port
156 resolv-retry infinite
157 nobind
158 persist-key
159 # persist-tun was here, but if the vpn goes down this makes
160 # the whole thing get stuck if the vpn is our default route
161 # unless we set a special route out just for the vpn.
162 # todo: investigate.
163 ca ca-$name.crt
164 cert $name.crt
165 key $name.key
166 # disabled for better performance
167 #comp-lzo
168 verb 3
169
170 # matching server config
171 cipher AES-256-CBC
172
173 # example config has the commented line, but this other thing looks stronger,
174 # and I've seen it in a vpn provider I trust
175 # ns-cert-type server
176 remote-cert-tls server
177
178 # more resilient when running as nonroot
179 persist-key
180
181 # See comments in server side configuration.
182 # The minimum of the client & server config is what is used by openvpn.
183 reneg-sec 432000
184
185 tls-auth ta-$name.key 1
186 EOF
187
188 if [[ $script ]]; then
189 $shell "tee -a /etc/openvpn/client/$name.conf" <<EOF
190 script-security 2
191 up "$script"
192 down "$script"
193 EOF
194
195 if [[ $client_host && $script ]]; then
196 $shell "dd of=$script" <$script
197 $shell "chmod +x $script"
198 fi
199 fi
200
201 if ! $rel; then
202 $shell 'cd /etc/openvpn; for f in client/*; do ln -sf $f .; done'
203 fi
204
205 if ! $rel; then
206 dirarg="-C $keydir"
207 fi
208
209 # bash or else we get motd spam. note sleep 2, sleep 1 failed.
210 $shell '[[ -e /etc/openvpn ]] || apt install openvpn'
211 hostssh="ssh $arg root@$host"
212 if ! $hostssh bash -s -- $server_name $common_name < $this_dir/client-cert-helper \
213 | $shell "id -u | grep -xF 0 || s=sudo; \$s tar xzv $dirarg"; then
214 echo $hostssh cat /tmp/vpn-mk-client-cert.log:
215 $hostssh cat /tmp/vpn-mk-client-cert.log
216 echo EOF for root@$host:/tmp/vpn-mk-client-cert.log
217 exit 1
218 fi
219
220
221
222 if ! $shell "test -s $f"; then
223 # if common name is not unique, you get empty file. and if we didn't silence
224 # build-key, you'd see an error "TXT_DB error number 2"
225 echo "$0: error: $f is empty or otherwise bad. is this common name unique?"
226 exit 1
227 fi