Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 61 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ sudo BACKUP_BUCKET=my-mediawiki-backups \

---

## Step 5b — Set up SSL

Port 80 is immediately redirected to HTTPS. The AMI ships with the `mod_ssl`
self-signed cert as a placeholder, so browsers will show a security warning
until certbot replaces it. Once DNS is pointed at the new instance and port 443
is reachable, obtain a real certificate:

```bash
sudo bash /opt/mediawiki-ami/setup/setup-ssl.sh
```

This runs `certbot --apache`, obtains a Let's Encrypt cert for `wiki.asmbly.org`,
updates the `:443` vhost in `mediawiki.conf`, and installs the renewal timer.

---

## Step 6 — Validate & cut over

```bash
Expand All @@ -139,7 +155,7 @@ When satisfied:

## Automated backups (new server)

The new server runs `scripts/backup/backup-with-retention.sh` nightly at 02:00 UTC
The new server runs `scripts/backup/backup-with-retention.sh` nightly at 08:00 UTC
via `/etc/cron.d/mediawiki-backup`. It uploads to S3 with GFS rotation:

| Tier | When | S3 prefix | Expires |
Expand All @@ -154,27 +170,53 @@ via `/etc/cron.d/mediawiki-backup`. It uploads to S3 with GFS rotation:

```
config/
mediawiki/LocalSettings.php ← Config-as-code; secrets injected by envsubst at build time
httpd/mediawiki.conf ← Apache vhost template
php/mediawiki.ini ← PHP tuning
cloudwatch/mediawiki-cwa.json ← CloudWatch agent config
cron/mediawiki-backup ← Cron job definitions
cron/mediawiki-jobs ← MediaWiki job runner cron
httpd/mediawiki.conf ← Apache vhost template
httpd/security.conf ← Apache security headers
logrotate/httpd-mediawiki ← Log rotation for Apache
logrotate/mediawiki-backup ← Log rotation for backups
mariadb/mariadb.repo ← MariaDB 10.11 yum repo
mariadb/mediawiki.cnf ← MariaDB tuning
mediawiki/LocalSettings.php ← Config-as-code; secrets injected by envsubst at build time
mediawiki/composer.local.json ← Composer local overrides
mediawiki/robots.txt ← robots.txt for the wiki
mediawiki/assets/ ← Static assets copied to DocumentRoot (logos, favicons)
php/mediawiki.ini ← PHP tuning
system/limits.conf ← OS limits
system/mediawiki-backup.sysconfig ← /etc/sysconfig for backup cron
system/sysctl.conf ← Kernel tuning

docs/
lifecycle.json ← S3 Lifecycle Rules (apply once to bucket)
s3-backup-setup.md ← Bucket setup walkthrough
lifecycle.json ← S3 Lifecycle Rules (apply once to bucket)
s3-backup-setup.md ← Bucket setup walkthrough
iam/README.md ← IAM setup guide
iam/oidc-trust-policy.json ← OIDC trust policy for GitHub Actions
iam/packer-policy.json ← Minimal IAM policy for Packer
iam/setup-oidc-role.sh ← Script to create OIDC role
iam/setup-vpc.sh ← Script to create VPC for Packer

packer/
mediawiki.pkr.hcl ← Packer HCL2 build template
variables.pkr.hcl ← Variable declarations
variables.pkrvars.hcl.example ← Copy → my.auto.pkrvars.hcl and fill in
mediawiki.pkr.hcl ← Packer HCL2 build template
variables.pkr.hcl ← Variable declarations
variables.pkrvars.example ← Copy → my.auto.pkrvars.hcl and fill in
scripts/
00-system.sh AL2025 base packages (lua, cronie, gettext, …)
01-php.sh PHP 8.3 + extensions
02-mariadb.sh MariaDB 10.11 LTS
03-httpd.sh Apache httpd + vhost
04-mediawiki.sh MW 1.43 core + envsubst → LocalSettings.php
05-extensions.sh REL1_43 extensions (Gerrit + GitHub)
06-finalize.sh Harden, clean, enable services
07-backup-setup.sh Install backup cron + /etc/sysconfig/mediawiki-backup
00-system.sh AL2023 base packages (lua, cronie, gettext, …)
01-php.sh PHP 8.3 + extensions
02-mariadb.sh MariaDB 10.11 LTS
03-httpd.sh Apache httpd + vhost
04-mediawiki.sh MW 1.43 core + envsubst → LocalSettings.php
05-extensions.sh REL1_43 extensions (Gerrit + GitHub)
06-finalize.sh Harden, clean, enable services
07-backup-setup.sh Install backup cron + /etc/sysconfig/mediawiki-backup

packer-test/
Dockerfile.test ← Container image for local packer script testing
test-local.sh ← Run packer scripts locally in Docker
mock-aws ← Stub AWS CLI for offline testing
container-systemctl ← systemctl shim for containers
test.env.example ← Copy → test.env and fill in

scripts/
inventory/gather-info.sh Server inventory report
Expand All @@ -184,12 +226,12 @@ scripts/
backup/config-export.sh Redacted config export → S3
restore/restore.sh Restore DB + images from S3 (reusable)
restore/upgrade-1.35-to-1.43.sh One-time: run update.php + post-upgrade maintenance
setup/setup-ssl.sh Post-launch: obtain Let's Encrypt cert via certbot

.github/workflows/
build-ami.yml Packer build on tag push / manual dispatch
packer-validate.yml Validate packer template on pull requests

output/
info.txt Inventory captured from the existing 1.35 server
```

---
Expand Down
76 changes: 63 additions & 13 deletions config/httpd/mediawiki.conf
Original file line number Diff line number Diff line change
@@ -1,42 +1,96 @@
# mediawiki.conf — Apache vhost for MediaWiki
# Uploaded to /tmp/ by Packer; installed by 03-httpd.sh.
# SSL is handled by Let's Encrypt (certbot) post-launch.
#
# HTTP redirects to HTTPS immediately. The HTTPS vhost uses the self-signed
# cert created by mod_ssl on first install; certbot replaces it post-launch:
# sudo bash /opt/mediawiki-ami/setup/setup-ssl.sh

# ── HTTP: redirect to HTTPS ───────────────────────────────────────────────────
<VirtualHost *:80>
ServerName wiki.asmbly.org
RewriteEngine On
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

# ── HTTPS: MediaWiki application ──────────────────────────────────────────────
<VirtualHost *:443>
ServerName wiki.asmbly.org
DocumentRoot /var/www/mediawiki

# Short URLs: /wiki/Article_Name → index.php
SSLEngine on
# Self-signed cert (installed by mod_ssl) — replaced by certbot post-launch
SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateKeyFile /etc/pki/tls/private/localhost.key

Header always set Strict-Transport-Security "max-age=63072000"

# ── Short URLs: /wiki/Article_Name → index.php ────────────────────────────
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-d
RewriteRule ^/wiki/(.*)$ /index.php [L]
RedirectMatch ^/$ /wiki/Main_Page

# ── DocumentRoot ──────────────────────────────────────────────────────────
<Directory /var/www/mediawiki>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>

# Protect sensitive files
<Files "LocalSettings.php">
# ── Block directories that contain PHP source / tools / installer ─────────
# None of these should ever be reachable via HTTP.
<DirectoryMatch "^/var/www/mediawiki/(cache|docs|includes|languages|maintenance|mw-config|tests|vendor)(/|$)">
Require all denied
</Files>
<Files "*.sql">
</DirectoryMatch>

# ── Block root-level non-web files ────────────────────────────────────────

# PHP: autoload.php is a Composer autoloader — not a web entry point.
# The 8 real entry points (index, api, load, rest, img_auth, thumb,
# thumb_handler, opensearch_desc) are intentionally left open.
<Files "autoload.php">
Require all denied
</Files>

# Uploads: no PHP execution
# Composer/build config files
<FilesMatch "^(LocalSettings\.php|composer\.local\.json|composer\.json|jsdoc\.json|docker-compose\.yml)$">
Require all denied
</FilesMatch>

# Documentation and data files with no web purpose
<FilesMatch "\.(md|sql|sample|yml|yaml)$">
Require all denied
</FilesMatch>

# ALLCAPS no-extension docs: COPYING, CREDITS, FAQ, HISTORY, INSTALL,
# RELEASE-NOTES-*, SECURITY, UPGRADE
<FilesMatch "^[A-Z][A-Z0-9_-]+$">
Require all denied
</FilesMatch>

# ── Images: accessible but no PHP execution, no .htaccess override ───────
# AllowOverride None prevents user-uploaded .htaccess files from
# changing security settings in this directory.
<Directory /var/www/mediawiki/images>
Options -Indexes
AllowOverride None
<FilesMatch "\.ph(p[0-9]?|tml)$">
Require all denied
</FilesMatch>
</Directory>

<Directory /var/www/mediawiki/cache>
# ── Extensions / skins: static assets only, no PHP execution ─────────────
# PHP files in these trees are loaded via include() by MW core, never
# via direct HTTP requests.
<Directory /var/www/mediawiki/extensions>
Options -Indexes
<FilesMatch "\.ph(p[0-9]?|tml)$">
Require all denied
</FilesMatch>
</Directory>

<Directory /var/www/mediawiki/skins>
Options -Indexes
<FilesMatch "\.ph(p[0-9]?|tml)$">
Require all denied
Expand All @@ -46,7 +100,3 @@
ErrorLog /var/log/httpd/mediawiki-error.log
CustomLog /var/log/httpd/mediawiki-access.log combined
</VirtualHost>

# HTTPS vhost — configured by certbot (Let's Encrypt) post-launch:
# sudo dnf install -y certbot python3-certbot-apache
# sudo certbot --apache -d wiki.asmbly.org
54 changes: 22 additions & 32 deletions config/mediawiki/LocalSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
# MW_SECRET_KEY — generate: openssl rand -hex 64
# MW_UPGRADE_KEY — generate: openssl rand -hex 16
# MW_SMTP_PASSWORD — Gmail app password for notification@asmbly.org
# ⚠ ROTATE THIS — old value was exposed in info.txt
# MW_DISCOURSE_SECRET — Discourse SSO shared secret
# ⚠ ROTATE THIS — old value was exposed in info.txt
#
# Derived from the MediaWiki 1.35.8 LocalSettings.php captured in output/info.txt
# and translated to 1.43 conventions. Changes from 1.35:
Expand Down Expand Up @@ -47,7 +45,7 @@
# =============================================================================

$wgLogo = "$wgScriptPath/ASMBLY_Avatar_135x135.png";
$wgFavicon = "$wgScriptPath/ASMBLY_avatar-1-150x150.png";
$wgFavicon = "$wgScriptPath/ASMBLY_Avatar-150x150.png";

$wgLogos = [
'1x' => "$wgScriptPath/ASMBLY_Avatar_135x135.png",
Expand Down Expand Up @@ -130,7 +128,6 @@
$wgEmailAuthentication = false;

# Gmail SMTP relay
# ⚠ MW_SMTP_PASSWORD must be rotated — old value was exposed in repo history
$wgSMTP = [
'host' => 'ssl://smtp.gmail.com',
'IDHost' => 'gmail.com',
Expand Down Expand Up @@ -211,7 +208,6 @@
wfLoadExtension( 'ParserFunctions' );
wfLoadExtension( 'PdfHandler' );
wfLoadExtension( 'Poem' );
wfLoadExtension( 'RenameUser' );
wfLoadExtension( 'ReplaceText' );
wfLoadExtension( 'SecureLinkFixer' );
wfLoadExtension( 'SpamBlacklist' );
Expand Down Expand Up @@ -284,40 +280,34 @@

# ── PluggableAuth v7 + DiscourseSsoConsumer ───────────────────────────────────
#
# PluggableAuth v7 (required for MW 1.39+) changed from flat global variables
# to a $wgPluggableAuth_Config array. The old 1.35 config used:
# $wgPluggableAuth_ButtonLabelMessage = "Log In With Discourse";
# which is removed in v7.
#
# The button label is now the array key in $wgPluggableAuth_Config.
# See: https://www.mediawiki.org/wiki/Extension:PluggableAuth#Configuration
#
# ⚠ MW_DISCOURSE_SECRET must be rotated — old value was exposed in repo history
wfLoadExtension( 'PluggableAuth' );
wfLoadExtension( 'DiscourseSsoConsumer' );

$wgPluggableAuth_Config = [
'Log In With Discourse' => [
'plugin' => 'DiscourseSsoConsumer',
'data' => [],
],
'Log In With Discourse' => [ 'plugin' => 'DiscourseSsoConsumer' ],
];

# Uncomment to allow local admin login alongside Discourse SSO:
# $wgPluggableAuth_EnableLocalLogin = true;

$wgDiscourseSsoConsumer_DiscourseUrl = "https://yo.asmbly.org";
$wgDiscourseSsoConsumer_SsoSharedSecret = "${MW_DISCOURSE_SECRET}";
$wgDiscourseSsoConsumer_ExposeName = true;
$wgDiscourseSsoConsumer_ExposeEmail = false;
$wgDiscourseSsoConsumer_EnableAutoRelogin = true;

# Discourse group → MediaWiki group mapping
# Discourse groups 'makers' and 'community' → MW 'editor'
# Discourse groups 'sysops' and 'leadership' → MW 'sysop' and 'bureaucrat'
$wgDiscourseSsoConsumer_GroupMaps = [
'editor' => [ 'makers', 'community' ],
'sysop' => [ 'sysops', 'leadership' ],
'bureaucrat' => [ 'sysops', 'leadership' ],
];
# DiscourseSsoConsumer uses a hook-based config (flat globals removed in 6.x).
# See: https://codeberg.org/centertap/DiscourseSsoConsumer#configure-discoursessoconsumer-using-a-hook-function
$wgHooks['DiscourseSsoConsumer_Configure'][] =
function ( array &$config ) {
$config['DiscourseUrl'] = 'https://yo.asmbly.org';
$config['Sso']['Enable'] = true;
$config['Sso']['SharedSecret'] = '${MW_DISCOURSE_SECRET}';
$config['Sso']['EnableAutoRelogin'] = true;
$config['User']['ExposeName'] = true;
$config['User']['ExposeEmail'] = false;
# Discourse group → MediaWiki group mapping
# 'makers' and 'community' → editor
# 'sysops' and 'leadership' → sysop + bureaucrat
$config['User']['GroupMaps'] = [
'editor' => [ 'makers', 'community' ],
'sysop' => [ 'sysops', 'leadership' ],
'bureaucrat' => [ 'sysops', 'leadership' ],
];
return true;
};

Empty file.
Binary file added config/mediawiki/assets/ASMBLY_Avatar_100x100.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added config/mediawiki/assets/ASMBLY_Avatar_135x135.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added config/mediawiki/assets/ASMBLY_Avatar_150x150.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added config/mediawiki/assets/ASMBLY_Avatar_202x202.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added config/mediawiki/assets/ASMBLY_Avatar_270x270.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions config/mediawiki/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# robots.txt for wiki.asmbly.org
#
# All article content is open to crawlers via BOTH URL forms:
# /wiki/Article_Title (short URL, canonical)
# /index.php?title=Article (legacy URL — must remain crawlable for SEO)
#
# We block only the dynamic/action URL variants that generate high server load
# without producing indexable content.
#
# References:
# https://www.mediawiki.org/wiki/Manual:Robots.txt
# https://en.wikipedia.org/robots.txt

User-agent: *

# ── Special pages ─────────────────────────────────────────────────────────────
# Dynamically generated; no indexable article content.
Disallow: /wiki/Special:
Disallow: /wiki/Special%3A
Disallow: /index.php?title=Special:
Disallow: /index.php?title=Special%3A

# ── Action pages (edit, history, info, raw, submit, etc.) ─────────────────────
# Per-request DB queries with no index value.
Disallow: /*?action=
Disallow: /index.php?action=

# ── Diff and revision-comparison pages ───────────────────────────────────────
Disallow: /*?diff=
Disallow: /index.php?diff=
Disallow: /index.php?oldid=

# ── Particular dynamic pages ───────────────────────────────────────
Disallow: /index.php?title=Help
Disallow: /index.php?title=Image
Disallow: /index.php?title=MediaWiki
Disallow: /index.php?title=Special:
Disallow: /index.php?title=Template

# ── Printable versions (duplicate content) ────────────────────────────────────
Disallow: /*?printable=yes

# ── Allow the Internet Archiver to index action=raw and thereby store the raw wikitext of pages ────────────────────────────────────
User-agent: ia_archiver
Allow: /*&action=raw

# ── Crawl-delay: limit load on a small-instance server ───────────────────────
Crawl-delay: 1
7 changes: 6 additions & 1 deletion packer-test/container-systemctl
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ _mariadb_stop() {

_httpd_start() {
echo "[systemctl-mock] Starting httpd directly (no systemd)..."
httpd -k start 2>/dev/null || apachectl start 2>/dev/null || true
httpd -t || { echo "[systemctl-mock] ERROR: httpd config check failed" >&2; return 1; }
httpd -k start 2>/dev/null || apachectl start 2>/dev/null || {
echo "[systemctl-mock] ERROR: httpd failed to start — check /var/log/httpd/error_log" >&2
cat /var/log/httpd/error_log 2>/dev/null | tail -20 || true
return 1
}
}

_httpd_stop() {
Expand Down
2 changes: 1 addition & 1 deletion packer/mediawiki.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ source "amazon-ebs" "mediawiki" {

launch_block_device_mappings {
device_name = "/dev/xvda"
volume_size = 30
volume_size = 16
volume_type = "gp3"
delete_on_termination = true
encrypted = false
Expand Down
Loading
Loading