Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de0882b
refactor: remove ubuntu 20.04
RobHooper Jul 30, 2026
3ea5d03
feat: Update default MySQL version
RobHooper Aug 3, 2026
750d09b
feat: Update default PHP version
RobHooper Aug 3, 2026
002aaa0
refactor: remove legacy apt repo configuration
RobHooper Aug 3, 2026
36ee7b8
chore: fix linting
RobHooper Aug 4, 2026
044886d
docs: New Linode interface
RobHooper Aug 4, 2026
79c2b97
feat: update locale on new servers
RobHooper Aug 4, 2026
9f39f95
refactor(firewall.sh): remove ubuntu 20.04
RobHooper Aug 4, 2026
7942ba1
feat: add chrony support for Ubuntu 26+
RobHooper Aug 4, 2026
764fae6
chore: remove legacy comment
RobHooper Aug 4, 2026
d02ec59
feat: depreciate smtp_use_tls
RobHooper Aug 6, 2026
2e137aa
feat: set MDStoreDir
RobHooper Aug 6, 2026
5a6c77d
feat: Update SSL cipher suites
RobHooper Aug 6, 2026
6efe696
feat: add fail2ban requirements
RobHooper Aug 6, 2026
13c19e0
feat: Support new Percona install name format
RobHooper Aug 6, 2026
3691326
docs: note MDStoreDir change
RobHooper Aug 6, 2026
3c8d5a9
Merge branch 'main' into ubuntu26.04
RobHooper Aug 6, 2026
9dde254
fix: handle non-zero exit from firewall.sh
RobHooper Aug 6, 2026
9f2361f
feat: Implement NTS time syncronisation
RobHooper Aug 7, 2026
82ee86d
Apply suggestions from code review
RobHooper Aug 7, 2026
5346b2a
Apply suggestions from code review
RobHooper Aug 7, 2026
4066a69
chore: fix typo
RobHooper Aug 7, 2026
8953010
docs(create_server): Align with order and labels on Linode
jpmckinney Aug 7, 2026
2ccac53
chore: remove depreciated mail setting
RobHooper Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions docs/deploy/create_server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ Create the server via the :ref:`host<hosting>`'s interface.
#. `Log into Linode <https://login.linode.com/login>`__
#. Click *Create Linode*

#. Set *Linux Distribution* to the latest Ubuntu LTS version
#. Set *Region* to *London, UK (eu-west)*
#. Set *Linux Distribution* to the latest Ubuntu LTS version
#. Select a *Linode Plan*
#. Set *Linode Label* to the server's FQDN (e.g. ``ocp99.open-contracting.org``)
#. Set *Add Tags* to either *Production* or *Development*
#. Set *Root Password* to a `strong password <https://www.lastpass.com/features/password-generator>`__, and save it to OCP's `LastPass <https://www.lastpass.com>`__ account
#. Under *Firewall*, if using Docker, click *Create Firewall* and :ref:`configure an external firewall from step 3<docker-firewall>`. Otherwise, select *No firewall - traffic is unprotected (not recommended)* from the dropdown.
#. Check *Backups*
#. Click *Create Linode* and wait a few minutes for the server to power on

Expand All @@ -58,11 +59,11 @@ Create the server via the :ref:`host<hosting>`'s interface.

#. Rename the "Swap Image" disk to "### MB Swap Image"

#. On the *Configurations* tab:
#. On the *Network* tab:

#. Click *Edit* for the "My Ubuntu ##.04 LTS Disk Profile" (or similar) configuration
#. Uncheck *Auto-configure networking* (skip if configuring a non-OCP server)
#. Click *Save Changes*
#. Click *Interface Settings*
#. Uncheck *Enable Network Helper* (skip if configuring a non-OCP server)
#. Click *Save*

#. Click *Power On*
#. Copy *SSH Access* to your clipboard
Expand All @@ -79,8 +80,6 @@ Create the server via the :ref:`host<hosting>`'s interface.

Linode can take a day to close the ticket. In the meantime, proceed with the instructions below. Once the ticket is closed, assign a specific address within the /64 block in the :doc:`network configuration<../develop/update/network>`.

#. If using Docker, :ref:`configure an external firewall<docker-firewall>`.

.. tab-item:: Hetzner Cloud
:sync: hetzner-cloud

Expand Down
14 changes: 14 additions & 0 deletions pillar/common.sls
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,28 @@ ssh:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG8dhMVvgH/tt9+VoyokyUg/iKVcZKMku8pYN6o8RoT8XKoyP/iyrUIl5HxolqIt+PJTpomYkA40eJ/0mN4/kRhr+tctZ+tUdo8/G8H42FG3McklL6XlwOdXRGIYC+NynF8YGws57J8YkM2oL9linkUZYpGpVkNew2aEg916HWWfGZktwuQa7knIwIhFr9FlvxxaZhdcQ7VJjnJOP0fLLr5WCVaiWDGjQ5cHJURcTBL+j+eTRpKFvk9BMKCAQyLkSEluT0QeESDMtR7sRHA54to1LDXRX0ky9cAQ6mxXWgpSpmHCuPVYpzOfoSd7b8aczDLUGBxq9EWOTS3UMUWJBX Yohanna (OCP)

# The default locale is en_GB rather than en_US for accidental, historical reasons.
{% if grains.osmajorrelease|int >= 26 %}
locale: en_US
{% else %}
locale: en_GB
{% endif %}

ntp:
- 0.uk.pool.ntp.org
- 1.uk.pool.ntp.org
- 2.uk.pool.ntp.org
- 3.uk.pool.ntp.org

# NTS used over NTP on Ubuntu 26.04+
nts:
time.cloudflare.com:
1.ntp.ubuntu.com:
2.ntp.ubuntu.com:
3.ntp.ubuntu.com:
4.ntp.ubuntu.com:
ntp-bootstrap.ubuntu.com:
context: iburst maxsources 1 nts certset 1

smtp:
relay: True
relay_address: noreply@noreply.open-contracting.org
Expand Down
24 changes: 19 additions & 5 deletions salt/apache/files/conf/letsencrypt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@
MDContactEmail sysadmin@open-contracting.org
MDCertificateAgreement accepted
MDCAChallenges http-01
{%- if grains.osmajorrelease|int >= 26 %}
{#- Store mod_md data outside /etc due to systemd filesystem sandboxing. #}
MDStoreDir /var/lib/apache2/md
{%- endif %}
{%- for directive, value in salt['pillar.get']('apache:modules:mod_md', {})|items %}
{{ directive }} {{ value }}
{%- endfor %}
</IfModule>

# generated 2023-06-28, Mozilla Guideline v5.7, Apache 2.4.52, OpenSSL 3.0.2, intermediate configuration
# https://ssl-config.mozilla.org/#server=apache&version=2.4.52&config=intermediate&openssl=3.0.2&guideline=5.7
# Omitted DHE-RSA-CHACHA20-POLY1305 (lowest priority).
<IfModule ssl_module>
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
{%- if grains.osmajorrelease|int >= 26 %}
# generated 2026-08-06, TLSRef Guideline v6.0, Apache 2.4.66, OpenSSL 3.5.5, intermediate config, gitrev=1b22dc6
# https://configurator.tlsref.org/#server=apache&version=2.4.66&config=intermediate&openssl=3.5.5&guideline=6.0
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1:secp384r1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
{%- else %}
# generated 2026-08-06, TLSRef Guideline v6.0, Apache 2.4.52, OpenSSL 3.0.2 (OLD: missing PQC hybrid MLKEMs), intermediate config, gitrev=1b22dc6
# https://configurator.tlsref.org/#server=apache&version=2.4.52&config=intermediate&openssl=3.0.2&guideline=6.0
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLOpenSSLConfCmd Curves X25519:prime256v1:secp384r1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
{%- endif %}
SSLHonorCipherOrder off
SSLSessionTickets off
</IfModule>
15 changes: 0 additions & 15 deletions salt/apache/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,10 @@
{{ unset_firewall('PUBLIC_HTTPS') }}
{% endif %}

# ondrej/apache2 is still needed on Ubuntu 20.04 for MDContactEmail.
# https://github.com/icing/mod_md/issues/203
apache2:
{% if grains.osmajorrelease in ('18', '20') %}
pkgrepo.managed:
- ppa: ondrej/apache2
{% endif %}
pkg.installed:
- pkgs:
- apache2
{% if grains.osmajorrelease in ('18', '20') %}
# Avoid "AH01882: Init: this version of mod_ssl was compiled against a newer library (OpenSSL 1.1.1g 21 Apr 2020,
# version currently loaded is OpenSSL 1.1.1 11 Sep 2018) - may result in undefined or erroneous behavior"
# https://github.com/open-contracting/deploy/issues/66#issuecomment-742898193
- libssl1.1
- openssl
- require:
- pkgrepo: apache2
{% endif %}
service.running:
- name: apache2
- enable: True
Expand Down
2 changes: 0 additions & 2 deletions salt/core/apt/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ needrestart:
pkg.removed:
- name: needrestart

# https://www.phusionpassenger.com/library/install/apache/install/oss/bionic/
# gnupg depends on dirmngr. gnupg2 is a dummy package for gnupg.
secure ppa:
pkg.installed:
- pkgs:
Expand Down
4 changes: 4 additions & 0 deletions salt/core/fail2ban/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ fail2ban:
[Definition]
failregex = ^<HOST> .* ".*" 404
ignoreregex =
- require:
- pkg: apache2
- require_in:
- file: /etc/fail2ban/jail.local
- watch_in:
Expand All @@ -37,6 +39,8 @@ fail2ban:
[Definition]
failregex = ^<HOST> .* FATAL: password authentication failed for user ".*"$
ignoreregex =
- require:
- pkg: postgresql
- require_in:
- file: /etc/fail2ban/jail.local
- watch_in:
Expand Down
4 changes: 2 additions & 2 deletions salt/core/firewall/files/firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ fi

echo_verbose "Get iptables location"
case "${ID}_${VERSION_ID}" in
ubuntu_24.04 | ubuntu_22.04 | ubuntu_20.04 | ubuntu_18.04 | debian_10 | debian_9 | debian_8)
ubuntu_26.04 | ubuntu_24.04 | ubuntu_22.04 | debian_13 | debian_12)
IPTABLESSAVLOC=/etc/iptables/rules.v4
IP6TABLESSAVLOC=/etc/iptables/rules.v6
;;
centos_7 | redhat-derivative_)
redhat-derivative_)
IPTABLESSAVLOC=/etc/sysconfig/iptables
IP6TABLESSAVLOC=/etc/sysconfig/ip6tables
;;
Expand Down
4 changes: 2 additions & 2 deletions salt/core/firewall/files/firewall_reset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ fi

echo_verbose "Get iptables location"
case "${ID}_${VERSION_ID}" in
ubuntu_22.04 | ubuntu_20.04 | ubuntu_18.04 | debian_10 | debian_9 | debian_8)
ubuntu_26.04 | ubuntu_24.04 | ubuntu_22.04 | debian_13 | debian_12)
IPTABLESSAVLOC=/etc/iptables/rules.v4
IP6TABLESSAVLOC=/etc/iptables/rules.v6
;;
centos_7 | redhat-derivative_)
redhat-derivative_)
IPTABLESSAVLOC=/etc/sysconfig/iptables
IP6TABLESSAVLOC=/etc/sysconfig/ip6tables
;;
Expand Down
3 changes: 3 additions & 0 deletions salt/core/firewall/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ iptables-persistent:
save iptables rules:
cmd.run:
- name: /home/sysadmin-tools/bin/firewall.sh
- success_retcodes:
Comment thread
RobHooper marked this conversation as resolved.
- 0
- 3 # Docker install detected, exiting early.
- onchanges:
- file: /home/sysadmin-tools/firewall-settings.local
- file: /home/sysadmin-tools/bin/firewall.sh
Expand Down
2 changes: 1 addition & 1 deletion salt/core/mail.sls
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ postfix:
smtp_sasl_auth_enable: "yes"
smtp_sasl_security_options: "noanonymous"
smtp_sasl_password_maps: "hash:/etc/postfix/sasl_passwd"
smtp_use_tls: "yes"
smtp_tls_security_level: "may"
smtp_tls_note_starttls_offer: "yes"
{%- if "relay_address" in pillar.smtp %}
smtp_generic_maps: "hash:/etc/postfix/generic"
Expand Down
3 changes: 3 additions & 0 deletions salt/core/ntp/files/ntp-pools.sources
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{%- for source, entry in pillar.nts | items %}
pool {{ source }} {{ entry.context | default("iburst maxsources 1 nts prefer") }}
{%- endfor %}
31 changes: 25 additions & 6 deletions salt/core/systemd/ntp.sls → salt/core/ntp/init.sls
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
# Configure an SNTP service.
{% if grains.osmajorrelease|int >= 26 %}
chrony:
service.running:
- name: chrony

chrony-reload:
cmd.wait:
- name: chronyc reload sources

/etc/chrony/sources.d/ntp-pools.sources:
Comment thread
RobHooper marked this conversation as resolved.
file.managed:
- source: salt://core/ntp/files/ntp-pools.sources
- template: jinja
- watch_in:
- cmd: chrony-reload

/etc/chrony/sources.d/ubuntu-ntp-pools.sources:
file.comment:
- regex: "^pool "
- backup: False
- watch_in:
- cmd: chrony-reload
{% else %}
systemd-timesyncd:
{% if grains['osrelease'] >= '20.04' %}
# timesyncd is built into systemd on older Ubuntu releases.
pkg.installed:
- name: systemd-timesyncd
{% endif %}
service.running:
- name: systemd-timesyncd
- enable: True
{% if grains['osrelease'] >= '20.04' %}
- require:
- pkg: systemd-timesyncd
{% endif %}

/etc/systemd/timesyncd.conf.d/customization.conf:
file.managed:
- source: salt://core/systemd/files/timesyncd.conf
- source: salt://core/ntp/files/timesyncd.conf
- template: jinja
- makedirs: True
- watch_in:
Expand All @@ -27,6 +45,7 @@ systemd-timesyncd:
- backup: False
- watch_in:
- service: systemd-timesyncd
{% endif %}

set timezone to utc:
timezone.system:
Expand Down
5 changes: 5 additions & 0 deletions salt/core/sshd/files/customization.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{#-
We set both PermitRootLogin and PasswordAuthentication for two reasons:
- PermitRootLogin adds a layer of security in case PasswordAuthentication is toggled on.
- While PermitRootLogin is set to "no" or "without-password", we can monitor it with our intrusion detection software.
-#}
PasswordAuthentication no
PermitRootLogin without-password
X11Forwarding no
25 changes: 1 addition & 24 deletions salt/core/sshd/init.sls
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
# We'll only be using SSH key authentication.
{% if grains['osrelease'] >= '20.04' %}
/etc/ssh/sshd_config.d/customization.conf:
file.managed:
- source: salt://core/sshd/files/customization.conf
- template: jinja
- watch_in:
- service: ssh_service

{% else %}
# We set both PermitRootLogin and PasswordAuthentication for two reasons:
#
# - PermitRootLogin adds a layer of security in case PasswordAuthentication is toggled on.
# - While PermitRootLogin is set to "no" or "without-password", we can monitor it with our intrusion detection software.
harden ssh configuration:
file.keyvalue:
- name: /etc/ssh/sshd_config
- key_values:
# Disable password authentication.
PasswordAuthentication: 'no'
# Force root logins with SSH keys.
PermitRootLogin: without-password
# Disable X11 forwarding.
X11Forwarding: 'no'
- separator: ' '
- uncomment: '# '
- key_ignore_case: True
- append_if_not_found: True
- watch_in:
- service: ssh_service
{% endif %}

# Restart the SSH service if the config changes.
ssh_service:
service.running:
Expand Down
4 changes: 0 additions & 4 deletions salt/docker/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@
docker:
pkgrepo.managed:
- humanname: Docker Official Repository
{% if grains.osmajorrelease|string in ('18', '20') %}
- name: deb [arch={{ grains.osarch }}] https://download.docker.com/{{ grains.kernel|lower }}/{{ grains.os|lower }} {{ grains.oscodename }} stable
{% else %}
- name: deb [arch={{ grains.osarch }} signed-by=/usr/share/keyrings/docker-keyring.gpg] https://download.docker.com/{{ grains.kernel|lower }}/{{ grains.os|lower }} {{ grains.oscodename }} stable
- aptkey: False
{% endif %}
- dist: {{ grains.oscodename }}
- file: /etc/apt/sources.list.d/docker.list
- key_url: https://download.docker.com/{{ grains.kernel|lower }}/{{ grains.os|lower }}/gpg
Expand Down
8 changes: 7 additions & 1 deletion salt/mysql/init.sls
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% set mysql_version = pillar.mysql.version|default('8.0')|quote %}
{% set mysql_version = pillar.mysql.version|default('8.4')|quote %}

# https://docs.saltproject.io/en/latest/ref/states/all/salt.states.mysql_database.html
mysql dependencies:
Expand All @@ -13,8 +13,14 @@ percona-release:
- sources:
- percona-release: https://repo.percona.com/apt/percona-release_latest.{{ salt['grains.get']('lsb_distrib_codename') }}_all.deb
cmd.run:
{% if pillar.mysql.version == '8.0' %}
# Legacy naming format
- name: percona-release setup ps{{ mysql_version|replace('.', '') }}
- creates: /etc/apt/sources.list.d/percona-ps-{{ mysql_version|replace('.', '') }}-release.list
{% else %}
- name: percona-release setup ps{{ mysql_version|replace('.', '') }}-lts --scheme https
- creates: /etc/apt/sources.list.d/percona-ps-{{ mysql_version|replace('.', '') }}-lts-release.list
{% endif %}
- require:
- pkg: percona-release

Expand Down
2 changes: 1 addition & 1 deletion salt/php-fpm/init.sls
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% set php_version = pillar.php.version|default('8.1')|quote %}
{% set php_version = pillar.php.version|default('8.5')|quote %}

include:
- apache.modules.proxy_fcgi
Expand Down
4 changes: 0 additions & 4 deletions salt/postgres/init.sls
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,8 @@ pgbadger:
postgresql:
pkgrepo.managed:
- humanname: PostgreSQL Official Repository
{% if grains.osmajorrelease|string in ('18', '20') %}
- name: deb https://apt.postgresql.org/pub/repos/apt {{ grains.oscodename }}-pgdg main
{% else %}
- name: deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] https://apt.postgresql.org/pub/repos/apt {{ grains.oscodename }}-pgdg main
- aptkey: False
{% endif %}
- dist: {{ grains.oscodename }}-pgdg
- file: /etc/apt/sources.list.d/psql.list
- key_url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
Expand Down
2 changes: 1 addition & 1 deletion salt/top.sls
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ base:
- core.mail
- core.motd
- core.network
- core.ntp
- core.reboot
- core.rsyslog
- core.sshd
- core.swap
- core.sysctl
- core.systemd.logind
- core.systemd.ntp

'cms':
- cms
Expand Down
Loading