#!/bin/sh # by Derek Sivers # Updated: 2024-03-25 # INSTALL: cd /root ; ftp https://sive.rs/ti.sh ; sh ti.sh # README: https://sive.rs/ti if [[ $(id -u) -ne 0 || $(uname) != "OpenBSD" ]]; then echo "must be run as root on OpenBSD" exit 1 fi # tiny files where your answers are saved, so you can run this again and not answer again # to add another user, do `rm /root/my/user*` then run this script again mkdir -p /root/my function my { echo "/root/my/$1" } # INITIAL SETUP if [ ! -f /usr/local/bin/curl ]; then echo "updating..." syspatch # disable IPv6 and sound rcctl disable slaacd sndiod # shorten motd for future logins cat /etc/motd | head -3 | grep -v '^$' > /tmp/motd mv /tmp/motd /etc/motd # install needed software pkg_add curl rsync--iconv radicale-2.1.12p5 links mutt--sasl dovecot fi # download config files set -A a pf.conf httpd.conf relayd.conf acme-client.conf .muttrc .mailcap smtpd.conf dovecot.conf hello.txt hello.pdf derek.jpg guitar.mp3 ymap.mp4 for x in "${a[@]}"; do if [ ! -f $x ]; then ftp https://sive.rs/file/$x fi done if [ -f pf.conf ]; then mv pf.conf /etc/pf.conf pfctl -f /etc/pf.conf fi # DOMAIN? if [ -f $(my domain) ]; then domain=$(cat $(my domain)) else printf "Your domain name? " read ui # strip whitespace, convert to lowercase, and remove 'www.' prefix domain=$(echo "$ui" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | sed 's/^www\.//') # save to a file for next time echo $domain > $(my domain) fi # USERNAME? if [ -f $(my user) ]; then user=$(cat $(my user)) else printf "Your user name? (one lowercase word, no spaces): " read ui # strip whitespace, convert to lowercase, and remove all but a-z and 0-9 user=$(echo "$ui" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g') # save to a file for next time echo $user > $(my user) fi # DOMAIN AND USER OK? if [ ! -f $(my userok) ]; then echo "Email and login will be $user@$domain" printf "Does that look right? (y/n) " read ui # strip whitespace, convert to lowercase, get just first letter yn=$(echo "$ui" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | cut -c1-1) if [ $yn == "y" ]; then # touch file for next time touch $(my userok) else rm $(my domain) $(my user) echo "Please run this script again." exit 1 fi fi # FULL NAME? if [ -f $(my userfullname) ]; then name=$(cat $(my userfullname)) else printf "Your full name? " read name # save to a file for next time echo "$name" > $(my userfullname) fi # IP ADDRESS? if [ -f $(my ip) ]; then ip=$(cat $(my ip)) else ip=$(ifconfig vio0 | grep inet | awk '/inet/ {print $2}') # alternate: curl -s icanhazip.com # save to a file for next time echo $ip > $(my ip) fi echo "IP address is $ip" # ADD USER if [[ ! -d /home/$user ]]; then echo "Create a secret password for $user, for login and email:" groupadd $user useradd -b /home -g $user -k /etc/skel -L staff -s /bin/ksh -d /home/$user -m -c "$name" $user passwd $user cat /root/.ssh/authorized_keys >> /home/$user/.ssh/authorized_keys echo "permit persist $user" >> /etc/doas.conf printf "\n\n#############################\n" echo "TEST #1: LOG IN" echo "Open a NEW terminal window on your computer, and type this:" echo "ssh $user@$ip" echo "" echo "It should say 'OpenBSD 7.4 (GENERIC.MP)' and 'Welcome to OpenBSD'." echo "After that works, come back to this terminal window." printf "I'll wait" loggedin="" while [[ $loggedin == "" ]]; do printf "." sleep 3 loggedin=$(w|grep ^$user) done echo "YOU DID IT! Logging out that user now, because it's time for..." # find new user's SSH session and kill it to log them out pid=$(ps aux|grep sshd|grep ^$user|tail -1|awk '{print $2}') kill -HUP $pid echo "PasswordAuthentication no" >> /etc/ssh/sshd_config rcctl restart sshd printf "\n\n#############################\n" echo "TEST #2: LOG IN WITHOUT A PASSWORD" echo "Because I just disabled passwords, for super-security, make sure you can" echo "log in with your SSH public key." echo "Back in that other terminal window on your computer, login again like this:" echo "ssh $user@$ip" echo "" echo "Again, it should say 'OpenBSD 7.4 (GENERIC.MP)' and 'Welcome to OpenBSD'." echo "" # if trouble: # sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/g" /etc/ssh/sshd_config # rcctl restart sshd echo "If it doesn't work, hit Ctrl-C here and use root user to copy the contents" echo "of your SSH public key (id_ed25519.pub) into /home/$user/.ssh/authorized_keys" printf "Waiting" loggedin="" while [[ $loggedin == "" ]]; do printf "." sleep 3 loggedin=$(w|grep ^$user) done echo "YOU DID IT! Logging out that user again." # find new user's SSH session and kill it to log them out pid=$(ps aux|grep sshd|grep ^$user|tail -1|awk '{print $2}') kill -HUP $pid # SECURE LOGIN sed -i "s/RootLogin yes/RootLogin no/g" /etc/ssh/sshd_config rcctl restart sshd echo "I have disabled the root user, for security, so never log in as 'root' again." echo "Only log in as $user from now on." echo "To run a super-user (root) command, just type 'doas' before it." fi # VULTR API KEY? # file contains their previous answer if [ -f $(my vultr) ]; then vultrapi=$(cat $(my vultr)) else printf "\n\n#############################\n" echo "YOU NEED TO DO THIS NOW:" echo "1. Log in to your account at vultr.com" echo "" echo "2. Go to https://my.vultr.com/settings/#settingsapi" echo "(Or to get there: on the far left, click 'Account'" echo "then to the right of it, click 'API' 3rd from bottom.)" echo "" echo "3. Under 'Access Control', under 'Enter your IPv4', add this:" echo "$ip / 32" echo "... then click the [Add] button" echo "" printf "4. Under 'Personal Access Token', copy your API key and paste it here: " # Repeat until correct API and Access Control apiok="" while [[ $apiok != "200" ]]; do read ui # strip whitespace vultrapi=$(echo "$ui" | tr -d '[:space:]') # if API key works, gets "200" HTTP response code apiok=$(curl -o /dev/null -s -w '%{http_code}' "https://api.vultr.com/v2/domains" -X GET -H "Authorization: Bearer $vultrapi") if [[ $apiok == "200" ]]; then echo "API works!" # save to a file for next time echo $vultrapi > $(my vultr) # ADD DOMAIN NOW domainadded=$(curl -o /dev/null -s -w '%{http_code}' "https://api.vultr.com/v2/domains/$domain" -X GET -H "Authorization: Bearer $vultrapi") # if domain is in DNS already, the API returns "200" HTTP response code if [[ $domainadded == "200" ]]; then echo "$domain is in Vultr DNS already." echo "Assuming this is from a previous installation, I'll delete the previous entries now and start fresh, OK?" echo "Hit [enter] if this is OK, or hit Ctrl-C now to stop and re-start the script." read ui res=$(curl -s "https://api.vultr.com/v2/domains/$domain" -X DELETE -H "Authorization: Bearer $vultrapi") fi echo "Adding $domain to Vultr DNS. A, CNAME, and MX to $ip." res=$(curl -s "https://api.vultr.com/v2/domains" -X POST -H "Authorization: Bearer $vultrapi" -H "Content-Type: application/json" --data "{\"domain\":\"$domain\", \"ip\":\"$ip\"}") else echo "Sorry. It's not authorizing. See steps 3 and 4, above." echo "Under 'Access Control', make sure it says $ip/32" printf "And copy+paste your long API key here: " fi done fi # WHICH INSTANCE ID? ("instance" is Vultr's name for this server) if [ -f $(my instance) ]; then instance=$(cat $(my instance)) else # JSON, so put each { on new line, grep for line with this IP address, then awk to extract "id":"id-is-here" instance=$(curl -s "https://api.vultr.com/v2/instances" -X GET -H "Authorization: Bearer $vultrapi" | perl -pe 's/{/\n{/g' | grep $ip | awk -F'"id":"|"' '/"id":"/ {print $2}') echo "Instance ID $instance, but you don't need this info." echo $instance > $(my instance) # SET REVERSE DNS: ($ip = $domain) res=$(curl -s "https://api.vultr.com/v2/instances/$instance/ipv4/reverse" -X POST -H "Authorization: Bearer $vultrapi" -H "Content-Type: application/json" --data "{\"ip\":\"$ip\", \"reverse\":\"$domain\"}") fi # STORAGE: if [ ! -f $(my storageno) ]; then # check each time in case they added new storage sd1=$(disklabel sd1 2>&1) if [[ $sd1 == *"Device not configured"* ]]; then echo "You do not have 'Block Storage' attached to this server." printf "Do you want encrypted file storage? (y/n) " read ui yn=$(echo "$ui" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | cut -c1-1) if [ $yn == "n" ]; then touch $(my storageno) else # Find unattached block: (separate JSON, grep for empty instance, awk to get ID) bid=$(curl -s "https://api.vultr.com/v2/blocks" -X GET -H "Authorization: Bearer $vultrapi" | perl -pe 's/{/\n{/g' | grep '"attached_to_instance":""' | head -1 | awk -F'"id":"|"' '/"id":"/ {print $2}') if [[ $bid == "" ]]; then echo "You don't have any available Block Storage. See https://sive.rs/ti" echo "Deploy Block Storage at https://my.vultr.com/blockstorage/ then run this script again." exit 1 else echo "Block ID $bid. Instance ID $instance. You don't need this info." res=$(curl -s "https://api.vultr.com/v2/blocks/$bid/attach" -X POST -H "Authorization: Bearer $vultrapi" -H "Content-Type: application/json" --data "{\"instance_id\":\"$instance\", \"live\":true}") # Prompt then reboot printf "\n\n#############################\n" echo "COME BACK TO CONTINUE:" echo "Storage attached, but now it needs to reboot, since it looks for storage at boot time." echo "After it reboots, wait a minute then type this to continue:" echo "ssh $user@$ip" echo "doas su" echo "cd" echo "sh ti.sh" echo "# OK? Hit [enter] now to let it reboot." read ui touch $(my rebooted) reboot exit 0 fi fi elif [[ $sd1 == *"duid: 0000000000000"* ]]; then echo 'RAID *' | disklabel -wAT- sd1 echo "Make a password for your encrypted storage:" bioctl -c C -l sd1a softraid0 echo '/ *' | disklabel -wAT- sd2 newfs sd2a mount /dev/sd2a /mnt chown -R $user:$user /mnt chmod 770 /mnt fi fi # TEST STORAGE if [[ ! -f $(my storageno) && ! -f $(my storageok) ]]; then echo '#!/bin/sh\ndoas bioctl -c C -l sd1a softraid0\ndoas mount /dev/sd2a /mnt\nls -l /mnt' > /usr/local/sbin/m echo '#!/bin/sh\ndoas umount /mnt\ndoas bioctl -d sd2\necho "unmounted"' > /usr/local/sbin/m-x chmod 755 /usr/local/sbin/m /usr/local/sbin/m-x echo "Testing that your encrypted storage works." # if not mounted already, mount it res=$(df|grep '/mnt$') if [[ $res == "" ]]; then echo "Your encrypted storage password:" bioctl -c C -l sd1a softraid0 mount /dev/sd2a /mnt chown -R $user:$user /mnt fi uploaded=$(ls /mnt) if [[ $uploaded != "" ]]; then echo "You've already uploaded to /mnt so we're good." else printf "Now upload anything to $user@$ip:/mnt/ while I wait" while [[ $uploaded == "" ]]; do printf "." sleep 3 uploaded=$(ls /mnt) done echo "YOU DID IT!" fi echo "Unmounting encrypted storage now." umount /mnt bioctl -d sd2 touch $(my storageok) fi if [ ! -f $(my rebooted) ]; then echo "Your server needs to reboot now, to apply the initial syspatch." echo "AFTER IT REBOOTS, TYPE THIS FROM YOUR TERMINAL TO LOG IN AGAIN:" echo "ssh $user@$ip" echo "" echo "AFTER LOGGING IN, TYPE:" echo "doas su" echo "cd" echo "sh ti.sh" echo "" echo "... so we can continue, OK? Hit [enter] now to reboot." read ui touch $(my rebooted) reboot exit 0 fi # NAMESERVERS HERE? ns=$(nslookup -type=NS $domain) if [[ $ns == *ns1.vultr.com* && $ns == *ns2.vultr.com* ]]; then echo "$domain nameservers are set to vultr.com." elif [[ $ns == *SERVFAIL* ]]; then echo "$domain nslookup returning mysterious 'SERVFAIL'" echo "Next steps might or might not work! If not, try again later." else printf "\n\n#############################\n" echo "YOU NEED TO DO THIS NOW:" echo "1. Log in to the website where you registered your $domain domain name." echo "(Example: godaddy.com namecheap.com porkbun.com bookmyname.com)" echo "" echo "2. Edit the DNS Name Servers (NS)" echo "They are currently set to:" echo "$ns" | awk -F'nameserver = ' '/nameserver = / {print $2}' | grep -v '^$' echo "" echo "3. Erase those and replace them with:" echo "ns1.vultr.com" echo "ns2.vultr.com" echo "" echo "4. Wait a while (1-24 hours) and run this script again." exit 0 fi # create cert-renew script if not there already if [ ! -f /usr/local/sbin/cert-renew ]; then echo "#!/bin/sh\nacme-client $domain\nrcctl restart relayd\nrcctl restart dovecot" > /usr/local/sbin/cert-renew chmod 700 /usr/local/sbin/cert-renew fi # add cert-renew to crontab if not there already res=$(crontab -l|grep cert-renew) if [[ $res == "" ]]; then (crontab -l 2>/dev/null; echo "11\t3\t*\t*\t5\t/usr/local/sbin/cert-renew") | crontab - fi # WEB if [ ! -f $(my webok) ]; then echo "Setting up https://$domain/ web server and secure certificate." sed -i s/example.com/$domain/g httpd.conf relayd.conf acme-client.conf cp httpd.conf relayd.conf acme-client.conf /etc/ rcctl enable httpd rcctl start httpd acme-client -v $domain rcctl enable relayd rcctl start relayd html=$(printf '\n%s\n

%s

\n

%s
\n%s@%s\n

\n

Test public files

' $domain $domain "$name" $user $domain $user $domain) echo $html > /var/www/htdocs/index.html rm -rf /var/www/htdocs/bgplg mkdir -p /var/www/htdocs/pub cp hello.txt hello.pdf derek.jpg guitar.mp3 ymap.mp4 /var/www/htdocs/pub/ chown -R $user:$user /var/www/htdocs printf "\n\n#############################\n" echo "TEST THIS NOW:" echo "Go to https://$domain/ now to verify the index.html placeholder." echo "It should say $domain with your name and $user@$domain." echo "Hit [enter] here if it's good, or Ctrl-C to quit and try again." read ui touch $(my webok) fi # RADICALE PER-USER if [ ! -d /var/db/radicale/collections/collection-root/$user ]; then sed -i 's/#type = none/type = htpasswd/g' /etc/radicale/config echo "Setting up Contacts and Calendar for user $user." echo "For Contacts and Calendar only, make a new password that’s easy to type on your phone." htpasswd /etc/radicale/users $user chown _radicale /etc/radicale/users rcctl enable radicale rcctl restart radicale echo '#!/bin/sh\ncp -r /var/db/radicale /home/$user/\nchown -R $user /home/$user' > /usr/local/sbin/radbak chmod 700 /usr/local/sbin/radbak # add Radicale backup to crontab if not there already res=$(crontab -l|grep radbak) if [[ $res == "" ]]; then (crontab -l 2>/dev/null; echo "9\t3\t*\t*\t*\t/usr/local/sbin/radbak") | crontab - fi # create for user mkdir -p /var/db/radicale/collections/collection-root/$user/{calendar,contacts} prop=$(printf '{"C:supported-calendar-component-set": "VEVENT", "D:displayname": "%s", "tag": "VCALENDAR"}' $domain) echo $prop > /var/db/radicale/collections/collection-root/$user/calendar/.Radicale.props prop=$(printf '{"D:displayname": "%s", "tag": "VADDRESSBOOK"}' $domain) echo $prop > /var/db/radicale/collections/collection-root/$user/contacts/.Radicale.props chown -R _radicale:_radicale /var/db/radicale chmod 600 /var/db/radicale/collections/collection-root/$user/calendar/.Radicale.props chmod 600 /var/db/radicale/collections/collection-root/$user/contacts/.Radicale.props printf "\n\n#############################\n" echo "YOU NEED TO DO THIS NOW:" echo "On your phone, manually add a CalDAV account for calendar." echo "Server: dav.$domain" echo "Username: $user" echo "Password: the one you just created" echo "Then, manually add a CardDAV account for contacts. Same info:" echo "Server: dav.$domain" echo "Username: $user" echo "Password: the one you just created" echo "" echo "AFTER THAT, TEST THE CALENDAR:" echo "On your phone, add a new (fake/test) calendar event to your $domain calendar." printf "I will tell you when I see it here" found="" while [[ $found == "" ]]; do printf "." sleep 3 found=$(find /var/db/radicale/collections/collection-root/$user/calendar/ -maxdepth 1 -type f -name '*.ics') done echo "" echo "Calendar entry added!" echo "NOW TEST THE CONTACTS:" echo "On your phone, add a new (fake/test) person to your $domain contacts." printf "I will tell you when I see it here" found="" while [[ $found == "" ]]; do printf "." sleep 3 found=$(find /var/db/radicale/collections/collection-root/$user/contacts/ -maxdepth 1 -type f -name '*.vcf') done echo "" echo "Contact added! Both work. Congratulations." fi # EMAIL # if conf files are here, email hasn't been set up yet if [ -f smtpd.conf ]; then sed -i s/example.com/$domain/g smtpd.conf mv smtpd.conf /etc/mail/ touch /etc/mail/secrets rcctl restart smtpd fi if [ -f dovecot.conf ]; then sed -i s/example.com/$domain/g dovecot.conf rm -rf /etc/dovecot/* mv dovecot.conf /etc/dovecot/ rcctl enable dovecot rcctl start dovecot fi # MAILJET SMTP AUTH CREDENTIALS if [[ ! -f $(my smtpapi) || ! -f $(my smtpsecret) ]]; then printf "\n\n#############################\n" echo "YOU NEED TO DO THIS NOW:" echo "1. Sign up at https://www.mailjet.com/" echo "Click [Get Started] and use your existing (gmail, etc) email not this." echo "" echo "2. Once logged in, get to https://app.mailjet.com/account/apikeys via:" echo " [Quick setup] → SMTP setup" echo " [See all API credentials]" echo " [Generate secret key]" echo "" printf "3. Copy the API Key and paste it here: " read ui smtpapi=$(echo "$ui" | tr -d '[:space:]') echo $smtpapi > $(my smtpapi) printf "4. Copy the Secret Key and paste it here: " read ui smtpsecret=$(echo "$ui" | tr -d '[:space:]') echo $smtpsecret > $(my smtpsecret) res=$(curl -s -X POST --user "$smtpapi:$smtpsecret" https://api.mailjet.com/v3/REST/sender --data "Email=*@$domain&EmailType=transactional&IsDefaultSender=true&Name=$domain") json=$(curl -s -X GET --user "$smtpapi:$smtpsecret" https://api.mailjet.com/v3/REST/dns/$domain) # extract DNS values to prove to Mailjet that you control this domain txthost=$(echo $json | perl -ne 'if(/"OwnerShipTokenRecordName"\s*:\s*"([^"]+)"/) { print $1 }') txthost="${txthost%?}" # remove final "." txtvalue=$(echo $json | perl -ne 'if(/"OwnerShipToken"\s*:\s*"([^"]+)"/) { print $1 }') data=$(printf '{"type":"TXT", "name":"%s", "data":"%s"}' $txthost "$txtvalue") cmd=$(printf 'curl -s "https://api.vultr.com/v2/domains/%s/records" -X POST -H "Authorization: Bearer %s" -H "Content-Type: application/json" --data '\''%s'\''' $domain $vultrapi "$data") eval "$cmd" echo "" # DKIM txthost="mailjet._domainkey.$domain" txtvalue=$(echo $json | perl -ne 'if(/"DKIMRecordValue"\s*:\s*"([^"]+)"/) { print $1 }') data=$(printf '{"type":"TXT", "name":"%s", "data":"%s"}' $txthost "$txtvalue") cmd=$(printf 'curl -s "https://api.vultr.com/v2/domains/%s/records" -X POST -H "Authorization: Bearer %s" -H "Content-Type: application/json" --data '\''%s'\''' $domain $vultrapi "$data") eval "$cmd" echo "" # SPF is same for everyone: curl -s "https://api.vultr.com/v2/domains/$domain/records" -X POST -H "Authorization: Bearer $vultrapi" -H "Content-Type: application/json" --data '{"type":"TXT", "name":"@", "data":"v=spf1 include:spf.mailjet.com ?all"}' echo "" # Validate res=$(curl -s -X POST --user "$smtpapi:$smtpsecret" https://api.mailjet.com/v3/REST/sender/*@$domain/validate) # SMTP RELAY: echo "mailjet $smtpapi:$smtpsecret" > /etc/mail/secrets chmod 640 /etc/mail/secrets chown root:_smtpd /etc/mail/secrets rcctl restart smtpd sed -i s/API1/$smtpapi/g .muttrc sed -i s/API2/$smtpsecret/g .muttrc fi # user account: if [[ ! -d /home/$user/Maildir ]]; then mkdir -p /home/$user/Maildir/{cur,new,tmp} chmod -R 700 /home/$user/Maildir cat .muttrc | sed "s/USER/$user/g" > /home/$user/.muttrc cp .mailcap /home/$user/ chown -R $user:$user /home/$user printf "\n\n#############################\n" echo "TEST EMAIL NOW:" echo "From an external email account (gmail, etc) send an email to $user@$domain" printf "I will tell you when I see it here" found="" while [[ $found == "" ]]; do printf "." sleep 2 found=$(find /home/$user/Maildir/new -type f) done echo "Got the email!" echo "" printf "\n\n#############################\n" echo "YOUR IMAP SETTINGS: (to check email from any device)" echo "Account type: IMAP" echo "Email address: $user@$domain" echo "Username: $user" echo "Password: the original secret user password you made" echo "Incoming mail server: $domain" echo "Outgoing mail server: $domain" echo "Connection security: SSL" echo "Authentication type: Basic authentication" echo "" echo "Alternatively, SSH into this server as $user and type:" echo "mutt" echo "" fi printf "\n\n#############################\n" echo "For updates and help, please see https://sive.rs/ti" echo "When your server is set up, please email me your URL." echo "(I worked hard on this, so I like to know it helped.)" echo "Or ask any questions, or give any suggestions." echo "" echo "Derek Sivers" echo "https://sive.rs/contact" printf "#############################\n\n"