This is the final project for Udacity's Full Stack Developer Nanodegree. This project sees the deployment of Project 2: Digital Bookshelf to a live, configured, and secured Ubuntu server hosted via Amazon Lightsail.
Complete. Successfully graded. Lightsail instance decommissioned. Warning: I'm no longer committing to this repo as it was for an assessment which is now complete. Some dependencies are now reporting vulnerabilities, so don't use anything from this repo in production code without validation.
Digital Bookshelf was live and has now been taken down following successful grading. I used one of my parked domains in order to enable SSL as I was uncomfortable having information passed around in plaintext over the Web. Following grading, I revoked the SSL certificates, deleted the Lightsail instance, and parked my domain again. However, I plan on hosting Digital Bookshelf under a subdomain on my personal Azure Ubuntu VM at a later time.
I chose to change the default SSH port to 1012. This is a well-known port that requires root access to listen on. This means if a malicious non-root user manages to access the server, they can't potentially crash the SSH daemon and replace it with their own.
The private key for user grader is surrendered as part of the submission process and is not present in this repository. I have also altered the server to allow grader to run sudo without having to enter their password. This is purely out of convenience and I would not normally recommend this for an online server.
Using the submitted private key, follow the instructions below to SSH in.
-
In your
~/.sshfolder, runsudo vim config. -
Append the following lines, save your changes, and close the file:
Host 13.236.68.94 User grader Port 1012 IdentityFile ~/.ssh/grader_id_rsa -
Run
sudo vim grader_id_rsa. -
Copy the submitted private key into the file, save your changes, and close the file.
-
Run
ssh grader@13.236.68.94.
fail2ban: bans IP addresses that have failed authentication in rapid succession.virtualenv: required to isolate the Digital Bookshelf application into its own environment.apache2: server that hosts the Digital Bookshelf application.postgresql-11: to provide the Digital Bookshelf with data persistence.libapache2-mod-wsgi-py3: the Python 3 version ofmod_wsgi; necessary for serving Python applications from Apache.python-certbot-apache: automates server SSL certificate creation, Apache configuration, and redirection (very useful!).
In the src/scripts directory are scripts for installing and uninstalling the application. It's strongly recommended you first set up your secret.cfg.json, secret.github_client_secrets.json, and secret.google_client_secrets.json files before running these scripts. This will allow the scripts to copy the required files and set up the database. In particular, remember to set up the connection string conn_string in secret.cfg.json. Below are some examples of connection string usings Postgres and SQLite.
- Example using Postgres:
postgres://bookshelfuser:<password>@localhost:5432/bookshelf - Example using SQLite and relative path:
sqlite:///bookshelf.db - Example using SQLite and absolute path:
sqlite:////usr/local/bookshelf/db/bookshelf.db
Before you can run these scripts, you'll need to mark them as executable. Run the following.
chmod +x cfg.shchmod +x install.shchmod +x uninstall.sh
Modify cfg.sh with the appropriate variables for your setup, e.g. where you want your Apache Bookshelf directory to be, where your WSGI scripts should be, where your virtualenv is located, etc. $installed_secrets_dir should match where you secret.* client secrets files are in secret.cfg.json. You may also need to modify the bookshelf.conf file for directory permissions if you install in other places than what this repository assumes.
If you're using Postgres, modify the install.sql file to have the password for your database user. The scripts themselves will autodetect whether you're using Postgres or not, but the database creation to Postgres will only occur if the connection string specifies localhost.
If you're using SQLite, the Apache user will need to own and have read-write permissions to the directory where the SQLite database is. The scripts will try to account for this by extracting the absolute path from the conn_string attribute in secret.cfg.json and changing the owner to $apache_user and $apache_user_group. Ensure that you verify these values are correct for your server setup.
chown -R www-data:www-data <your_sqlite_dir>
chmod -R u+w <your_sqlite_dir>A number of changes to the Digital Bookshelf application were necessary to prepare it for deployment. All of these improvements have been backported to the Project 2 repository.
- Added
psycopg2-binarytorequirements.txtto enable SQLAlchemy to use PostgreSQL. - Created a new module
cfgto hold configuration information located insecret.cfg.json, such as absolute paths tosecret.*files, the server session key, and the database connection string. - Refactored the existing codebase (mainly the
dalandapp) modules to use the newcfgmodule for configuration data and to remove relative file paths that were causing issues. - Refactored
main.pyto export theflaskapp (required for the WSGI script), remove dynamic session key generation, and not run in debug server mode unless it is directly invoked.
The order this list is in is not the order I took these actions in due to the fact I often discovered issues after the logical sequence of steps were undertaken, and sometimes some experimentation was required. I've nonetheless ordered this list in a logical sequence for future reference.
During development of this project, sudo apt update and sudo apt upgrade were regularly used to keep the software up-to-date.
- Enabled port 443 in Amazon Networking tab for SSL.
- Set the timezone to Australian Central Standard Time (ACST -- GMT+9:30):
sudo dpkg-reconfigure tzdata. - Enabled UFW:
- Enabled SSH:
sudo ufw allow ssh. - Enabled HTTP:
sudo ufw allow http. - Enabled HTTPS:
sudo ufw allow https. - Enabled NTP:
sudo ufw allow ntp. - Enable alternate SSH port:
sudo ufw allow 1012. - Disabled incoming traffic by default:
sudo ufw default deny incoming. - Enabled outgoing traffic by default:
sudo ufw default allow outgoing. - Enable UFW:
sudo ufw enable.
- Enabled SSH:
- Disabled SSH root login.
- Edit global SSHD config file:
sudo vim /etc/ssh/sshd_config. - Edit
PermitRootLoginsetting to bePermitRootLogin no.
- Edit global SSHD config file:
- Change default SSH port to 1012:
- Snapshot VM in case we lock ourselves out.
- Double-check port 1012 is enabled in UFW:
sudo ufw status. - Open port 1012 in Amazon networking tab.
- Open
sshd_config:sudo vim /etc/ssh/sshd_config. - Change
# Port 22toPort 1012. - Restart SSHD:
service sshd restart. - Connect as
ubuntuto confirm change is nominal. - Deny port 22:
sudo ufw deny ssh. - Remove SSH from allowed firewall ports in Lightsail.
- Installed fail2ban:
sudo apt install fail2ban. - Created and configured user grader.
- Create user:
adduser grader. - Add grader to
sudogroup:usermod -aG sudo grader. - Login as grader:
su - grader. - Create
.ssh:mkdir ~/.ssh. - Generate keypair for grader:
ssh-keygen -t rsa -b 4096 -C "grader@grader.com". - Copy
id_rsalocally and thenrm -rf ~/.ssh/id_rsa. - Rename
id_rsa.pubtoauthorized_keys:sudo mv ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys. - Confirm
.sshhas right permissions:sudo chmod 700 ~/.ssh. - Confirm
authorized_keyshas right permissions:sudo chmod 644 ~/.ssh/authorized_keys. - Remove need to enter password every time
sudois invoked:sudo visudo- Append
grader ALL=(ALL) NOPASSWD:ALLand save. - Connect as grader and test.
- Create user:
- Installed
virtualenv:sudo apt install virtualenv. - Installed Apache2:
sudo apt install apache2. - Installed PostgreSQL:
- Install certificates:
sudo apt-get install curl ca-certificates. - Add key:
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -. - Create
/etc/apt/sources.list.d/pgdg.list:sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'. - Update packages:
sudo apt-get update. - Install PostgreSQL:
sudo apt-get install postgresql-11. - Verify with
sudo -u postgres psql.
- Install certificates:
- Created database and configured PostgreSQL user:
- Start PSQL:
sudo -u postgres psql. - Create database:
create database bookshelf;. - Create user:
create user bookshelfuser with encrypted password '<password>';. - Connect to bookshelf DB:
\connect bookshelf. - Grant privileges to user:
grant all privileges on database bookshelf to bookshelfuser;grant all privileges on all tables in schema public to bookshelfuser;grant all privileges on all functions in schema public to bookshelfuser;
- Start PSQL:
- Installed
mod-wsgifor Python 3:sudo apt-get install libapache2-mod-wsgi-py3. - Enabled Apache URL Rewrite module:
sudo a2enmod rewrite. - Prepared web application:
- Clone this repo.
- Create web app directory:
var/www/bookshelf. - Copy
src/svr/*tovar/www/bookshelf. - Create
virtualenvinvar/www/bookshelf:sudo virtualenv -p python3 env. - Change owner to install dependencies:
chown -R ubuntu:ubuntu env. - Activate environment:
source env/bin/activate. - Install Python dependencies:
pip install -r requirements.txt. - Create database:
python create_db.py. - Configure
cfg/secret.cfg.json. - Remove unnecessary files:
create_db.pyexample.secret.github_client_secrets.pytests/requirements.txtcfg/example.secret.cfg.json
- Add
bookshelf.confto/etc/apache2/sites-available. - Add
bookshelf.wsgito/usr/local/www/wsgi-scripts. - Disable
defaultsite:sudo a2dissite 000-default.conf. - Enable
bookshelfapp:sudo a2ensite bookshelf.conf. - Restart Apache2:
sudo systemctl restart apache2.
- Installed
python-certbot-apacheand auto-generated SSL certificates and enabled URL rewrite from HTTP to HTTPS.
- Use
mod_wsgi-expressinstead of having to configuremod_wsgiand Apache2 the traditional way. - Run
mod_wsgias a daemon using a different system account that only has permissions to the assigned Apache virtual directory. This means that if the process ever gets compromised, it would only be able to affect the Digital Bookshelf application and database. - Containerize the app with Docker, and use Apache2 or nginx on the server as a reverse proxy to the hosted container. This allows multiple container-hosted applications to run relatively isolated from each other. I regard this to be the ideal setup.
A long list of resources that either directly assisted me or gave me ideas on improving the deployment process.
- https://linuxize.com/post/how-to-create-a-sudo-user-on-ubuntu/
- https://linuxize.com/post/how-to-add-and-delete-users-on-ubuntu-18-04/
- https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart
- https://superuser.com/questions/77617/how-can-i-create-a-non-login-user
- https://www.cyberciti.biz/faq/linux-unix-running-sudo-command-without-a-password/
- https://superuser.com/questions/77617/how-can-i-create-a-non-login-user
- https://linuxize.com/post/how-to-set-up-ssh-keys-on-ubuntu-1804/
- https://mediatemple.net/community/products/dv/204643810/how-do-i-disable-ssh-login-for-the-root-user
- https://askubuntu.com/questions/1962/how-can-multiple-private-keys-be-used-with-ssh
- https://superuser.com/questions/215504/permissions-on-private-key-in-ssh-folder
- https://www.cyberciti.biz/tips/setup-ssh-to-run-on-a-non-standard-port.html
- https://au.godaddy.com/help/changing-the-ssh-port-for-your-linux-server-7306
- https://www.cyberciti.biz/faq/create-ssh-config-file-on-linux-unix/
- https://serverfault.com/questions/189282/why-change-default-ssh-port
- https://unix.stackexchange.com/questions/2942/why-change-default-ssh-port
- https://linuxhint.com/ufw_list_rules/
- https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-18-04
- http://tangothu.github.io/blog/2016/08/16/python-How-To-Serve-Python-Flask-Application-Using-mod_wsgi-express/
- https://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIDaemonProcess.html
- https://serverfault.com/questions/294101/wsgidaemonprocess-specifying-a-user
- https://stackoverflow.com/questions/22346618/mod-wsgi-user-option-in-wsgidaemonprocess-doesnt-work
- https://www.shellhacks.com/modwsgi-hello-world-example/
- https://modwsgi.readthedocs.io/en/develop/user-guides/quick-configuration-guide.html
- https://unix.stackexchange.com/questions/38978/where-are-apache-file-access-logs-stored
- https://stackoverflow.com/questions/42260451/proper-write-permissions-for-apache-user-with-sqlite
- https://cloudkul.com/blog/apache-virtual-hosting-with-different-users/
- https://hostadvice.com/how-to/how-to-enable-apache-mod_rewrite-on-an-ubuntu-18-04-vps-or-dedicated-server/
- https://certbot.eff.org/lets-encrypt/ubuntubionic-apache.html
- https://www.ssllabs.com/ssltest/analyze.html
- https://security.stackexchange.com/questions/166684/should-i-revoke-no-longer-used-lets-encrypt-certificates-before-destroying-them
- https://letsencrypt.org/docs/revoking/
- https://wiki.postgresql.org/wiki/Apt
- https://suhas.org/sqlalchemy-tutorial/
- https://stackoverflow.com/questions/19260067/sqlalchemy-engine-absolute-path-url-in-windows
- https://www.digitalocean.com/community/tutorials/how-to-secure-postgresql-on-an-ubuntu-vps
- https://www.compose.com/articles/using-postgresql-through-sqlalchemy/
- https://landoflinux.com/linux_bash_scripting_structure.html
- https://stackoverflow.com/questions/19306771/get-current-users-username-in-bash
- https://stackoverflow.com/questions/5228345/how-to-reference-a-file-for-variables-using-bash
- https://unix.stackexchange.com/questions/42847/are-there-naming-conventions-for-variables-in-shell-scripts
- https://stackoverflow.com/questions/9772036/pass-all-variables-from-one-shell-script-to-another
- https://stackoverflow.com/questions/17622106/variable-interpolation-in-shell
- https://stackoverflow.com/questions/20615217/bash-bad-substitution
- https://stackoverflow.com/questions/3643848/copy-files-from-one-directory-into-an-existing-directory
- http://linuxcommand.org/lc3_wss0120.php
- https://stackoverflow.com/questions/20858381/what-does-bash-c-do
- https://stackoverflow.com/questions/3924182/how-the-does-keyword-if-test-if-a-value-is-true-of-false
- http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_02.html
- https://stackoverflow.com/questions/10376206/what-is-the-preferred-bash-shebang/10383546#10383546
- https://stackoverflow.com/questions/2237080/how-to-compare-strings-in-bash
- https://stackoverflow.com/questions/4749330/how-to-test-if-string-exists-in-file-with-bash
- https://stackabuse.com/substrings-in-bash/
- http://www.robelle.com/smugbook/regexpr.html
- https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-a-bash-variable
- https://www.cyberciti.biz/faq/how-to-use-sed-to-find-and-replace-text-in-files-in-linux-unix-shell/
- https://stackoverflow.com/questions/16153446/bash-last-index-of
- https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux
- https://unix.stackexchange.com/questions/423329/trivial-rm-rf-command-destroys-my-operating-system-in-a-testing-machine
- https://stackoverflow.com/questions/3685970/check-if-a-bash-array-contains-a-value
- https://ryanstutorials.net/linuxtutorial/cheatsheetgrep.php
- https://stackoverflow.com/questions/10552711/how-to-make-if-not-true-condition
- https://stackoverflow.com/questions/1378274/in-a-bash-script-how-can-i-exit-the-entire-script-if-a-certain-condition-occurs
- https://blender.stackexchange.com/questions/31964/problem-with-paths-in-my-script-relative-to-local-python-file
- https://stackoverflow.com/questions/50335676/sudo-privileges-within-python-virtualenv
- https://askubuntu.com/questions/323131/setting-timezone-from-terminal
- https://www.cyberciti.biz/faq/unix-linux-check-if-port-is-in-use-command/
- https://superuser.com/questions/260925/how-can-i-make-chown-work-recursively