Skip to content

Commit

Permalink
Add support for RHEL8 (#89)
Browse files Browse the repository at this point in the history
* Add support for RHEL8

This change adds support for running this application on RHEL8.

The fabfile.py configuration and deployment scripts are simplified;
RHEL8 includes Python 3.6 so we no longer need to install it, nor a
different version of SQLite. We can also use Node 18 instead of Node 16.

The Python application code also needs to be modified to use a more
secure version of hashlib.md5 to comply with FIPS mode. See
http://blog.serindu.com/2019/11/12/django-in-fips-mode/ for background
and the implemented workaround.

* Bump Node version to 20
  • Loading branch information
chosak authored Feb 12, 2024
1 parent 7b8b1e7 commit bf0bd8c
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 51 deletions.
60 changes: 9 additions & 51 deletions fabfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,12 @@

DEPLOY_ROOT = "/opt"

# Node 18 doesn't seem to work on RHEL 7.
# https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V18.md#toolchain-and-compiler-upgrades
NODE_VERSION = "16"
NODE_VERSION = "20"

SQLITE_VERSION = "3390200"
SQLITE_BASENAME = f"sqlite-autoconf-{SQLITE_VERSION}"
SQLITE_INSTALL_ROOT = f"{DEPLOY_ROOT}/{SQLITE_BASENAME}"

PYTHON_VERSION = "3.6.15"
PYTHON_BASENAME = f"Python-{PYTHON_VERSION}"
PYTHON_INSTALL_ROOT = f"{DEPLOY_ROOT}/{PYTHON_BASENAME}"

SOURCE_PARENT = f"{DEPLOY_ROOT}/cfpb"
SOURCE_REPO = "https://github.com/cfpb/website-indexer.git"
SOURCE_DIRNAME = "website-indexer"
Expand Down Expand Up @@ -65,55 +59,13 @@ def configure(conn):
)
conn.sudo("yum install -y nodejs")

# Install the Yarn package repository.
# https://classic.yarnpkg.com/lang/en/docs/install/#centos-stable
conn.run(
"curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo"
)
conn.sudo("yum install -y yarn")

# Install git to be able to clone source code repository.
conn.sudo("yum install -y git")

# Set up deploy root and grant permissions to deploy user.
conn.sudo(f"mkdir -p {DEPLOY_ROOT}")
conn.sudo(f"chown -R {conn.user}:{conn.user} {DEPLOY_ROOT}")

# Build and install SQLite (needs to happen before installing Python).
conn.sudo("yum install -y gcc sqlite-devel")
with conn.cd(DEPLOY_ROOT):
conn.run(f"curl -O https://www.sqlite.org/2022/{SQLITE_BASENAME}.tar.gz")
conn.run(f"tar xzf {SQLITE_BASENAME}.tar.gz")
conn.run(f"rm {SQLITE_BASENAME}.tar.gz")

with conn.cd(SQLITE_INSTALL_ROOT):
conn.run("./configure && make")

# https://github.com/pyinvoke/invoke/issues/459
conn.sudo(f'bash -c "cd {SQLITE_INSTALL_ROOT} && make install"')

# Build and install Python 3.
# This sets /usr/local/bin python and python3 commands to point to Python 3.
# This doesn't update /usr/bin/python (used by sudo)
conn.sudo("yum install -y openssl-devel bzip2-devel libffi-devel")

with conn.cd(DEPLOY_ROOT):
conn.run(
f"curl -O https://www.python.org/ftp/python/{PYTHON_VERSION}/{PYTHON_BASENAME}.tgz"
)
conn.run(f"tar xzf {PYTHON_BASENAME}.tgz")
conn.run(f"rm {PYTHON_BASENAME}.tgz")

with conn.cd(PYTHON_INSTALL_ROOT):
conn.run("LD_RUN_PATH=/usr/local/lib ./configure --enable-optimizations")

# https://github.com/pyinvoke/invoke/issues/459
conn.sudo(
f"bash -c "
f'"cd {PYTHON_INSTALL_ROOT} && LD_RUN_PATH=/usr/local/lib make install"'
)
conn.sudo("ln -sf /usr/local/bin/python3 /usr/local/bin/python")


@task
def deploy(conn):
Expand All @@ -131,8 +83,9 @@ def deploy(conn):

# Build the viewer app and update any dependencies.
with conn.cd(SOURCE_ROOT):
conn.sudo("corepack enable")
conn.run("yarn && yarn build")
conn.run("python -m venv venv")
conn.run("python3 -m venv venv")

with conn.prefix("source venv/bin/activate"):
conn.run("pip install -r requirements/base.txt")
Expand Down Expand Up @@ -183,7 +136,7 @@ def deploy(conn):

# SELinux: Allow logrotate to write to log files.
# This gets persisted to /etc/selinux/targeted/contexts/files/file_contexts.local
conn.sudo(f"semanage fcontext -a -t var_log_t '{LOG_DIR}(/.*)?'")
conn.sudo(f"semanage fcontext -m -t var_log_t '{LOG_DIR}(/.*)?'")

# Configure gunicorn to run via systemd.
print("Configuring gunicorn service")
Expand Down Expand Up @@ -218,4 +171,9 @@ def deploy(conn):
conn.sudo(f"systemctl restart {SYSTEMD_SERVICE}")

conn.sudo(f"systemctl status {SYSTEMD_SERVICE}")

# Open firewall so gunicorn can run on port 8000.
conn.sudo("firewall-cmd --zone=public --permanent --add-port 8000/tcp")
conn.sudo("firewall-cmd --reload")

print("Done!")
49 changes: 49 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

import os
import sys
from pathlib import Path
Expand Down Expand Up @@ -160,3 +161,51 @@
},
},
}

# Monkey patch hashlib.md5 for FIPS mode compliance on RHEL8.
# http://blog.serindu.com/2019/11/12/django-in-fips-mode/
import hashlib
import importlib


def _non_security_md5(*args, **kwargs):
kwargs["usedforsecurity"] = False
return hashlib.md5(*args, **kwargs)


def monkey_patch_md5(modules_to_patch):
"""Monkey-patch calls to MD5 that aren't used for security purposes.
Sets RHEL's custom flag `usedforsecurity` to False allowing MD5 in FIPS mode.
`modules_to_patch` must be an iterable of module names (strings).
Modules must use `import hashlib` and not `from hashlib import md5`.
"""
# Manually load a module as a unique instance
# https://stackoverflow.com/questions/11170949/how-to-make-a-copy-of-a-python-module-at-runtime
HASHLIB_SPEC = importlib.util.find_spec("hashlib")
patched_hashlib = importlib.util.module_from_spec(HASHLIB_SPEC)
HASHLIB_SPEC.loader.exec_module(patched_hashlib)

patched_hashlib.md5 = _non_security_md5 # Monkey patch MD5

# Inject our patched_hashlib for all requested modules
for module_name in modules_to_patch:
module = importlib.import_module(module_name)
module.hashlib = patched_hashlib


modules_to_patch = [
"django.contrib.staticfiles.storage",
"django.core.cache.backends.filebased",
"django.core.cache.utils",
"django.db.backends.utils",
"django.db.backends.sqlite3.base",
"django.utils.cache",
]

try:
import hashlib

hashlib.md5()
except ValueError:
monkey_patch_md5(modules_to_patch)

0 comments on commit bf0bd8c

Please sign in to comment.