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