Hardening Tomcat / Remediate Tomcat
Linux · Apache Tomcat · Security Engineering · 2026
Hardening Tomcat bukan soal mengganti satu-dua parameter lalu menyebut server aman. Yang saya cari justru konfigurasi yang bisa dipertanggungjawabkan: tahu baseline-nya, paham alasan tiap perubahan, punya bukti pengujian, dan tetap bisa rollback kalau ada yang tidak sesuai.
/opt/tomcat, service tomcat, dan port 8080 adalah placeholder.Checklist: yang diperiksa, yang diubah, dan alasannya
Sebelum menulis skrip, saya pisahkan dulu exposure, runtime configuration, access control, dan operasional. Kalau semuanya langsung ditembak sekaligus, saat ada error kita susah menentukan perubahan mana yang menyebabkannya.
| Area | Target | Pendekatan | Catatan |
|---|---|---|---|
| Versi & CVE | Rilis 9.0.x yang sudah mendapat perbaikan terbaru | Bandingkan advisory resmi dan uji upgrade | Configuration hardening tidak menggantikan security update |
| Service account | Non-root, hak tulis minimum | Dedicated user dan file permission | Direktori webapps/config jangan writable oleh runtime bila tidak diperlukan |
| Network connector | Port dan interface minimum | Batasi bind, firewall, reverse proxy | Jangan bind ke loopback jika LB berada di host lain |
| Shutdown port | Nonaktif bila mekanisme stop mendukung | Server port="-1" |
Uji systemd terlebih dahulu |
| Deployment | Tidak menerima deploy liar | autoDeploy=false, deployXML=false |
Validasi deployment pipeline; jangan asal ubah deployOnStartup |
| Banner/error | Informasi internal minim | Connector server header dan ErrorReportValve | Bukan pengganti patching, WAF atau auth |
| Request parsing | Header ilegal ditolak | rejectIllegalHeader=true |
Sudah default pada versi modern; tetap audit |
| Cookie & session | HttpOnly, Secure/SameSite bila sesuai |
context.xml dan pengujian browser |
Secure/SameSite perlu evaluasi HTTPS offload/SSO |
| Log & monitoring | Audit trail tersedia tanpa rahasia | AccessLogValve, journal, retensi | Jangan log token, password atau cookie autentikasi |
| TLS / mTLS | Protokol, trust CA dan client auth sesuai model | Evaluasi connector/proxy dan sertifikat | Opsional; clientAuth tidak boleh diwajibkan tanpa kebutuhan |
Topologi referensi dan batas kepercayaan
Contoh ini memakai reverse proxy sebagai pintu masuk. Tomcat tidak otomatis menjadi aman karena berada di belakang proxy: trafik dari proxy ke aplikasi tetap perlu ACL, dan header X-Forwarded-* tidak boleh dipercaya dari klien sembarangan.
Kalau tanpa reverse proxy, apa yang berubah?
Tomcat dapat melayani TLS sendiri menggunakan SSLHostConfig, tetapi hardening TLS, sertifikat, timeout, pembatasan akses dan observabilitas harus ditangani pada connector Tomcat. Jangan menggunakan contoh connector HTTP loopback untuk menggantikan konfigurasi HTTPS yang sedang berjalan.
Flowchart perubahan yang saya pakai
Urutannya penting. Perubahan yang kelihatannya kecil tetap harus lewat review diff dan pengujian fungsional. Flow berikut menghindari pola “edit XML, restart, lalu berharap”.
1. Ambil baseline sebelum menyentuh konfigurasi
Audit versi, socket, user service, dan XML
Skrip ini read-only. Hasilnya dipakai sebagai catatan kondisi awal sekaligus bahan change record. Jalankan di host yang memang Anda kelola; output versi, user dan listener bukan untuk diumbar ke publik.
#!/usr/bin/env bash
# tomcat-audit.sh - read-only baseline collection
set -Eeuo pipefail
BASE="${CATALINA_BASE:-/opt/tomcat}"
CONF="$BASE/conf"
SERVICE="${TOMCAT_SERVICE:-tomcat}"
printf '== Environment ==\n'
id; java -version 2>&1 | head -n 3
printf '\n== Tomcat binary ==\n'
if [[ -x "$BASE/bin/version.sh" ]]; then
"$BASE/bin/version.sh" 2>&1 | head -n 14
fi
printf '\n== Listener ==\n'
ss -lntp || true
printf '\n== systemd ==\n'
systemctl show "$SERVICE" -p User -p Group -p ExecStart -p FragmentPath 2>/dev/null || true
printf '\n== Config file access ==\n'
for f in "$CONF/server.xml" "$CONF/context.xml" "$CONF/web.xml"; do
[[ -f "$f" ]] && stat -c '%A %U:%G %n' "$f"
done
printf '\n== Configuration markers ==\n'
python3 - "$CONF/server.xml" <<'PY_AUDIT'
import sys, xml.etree.ElementTree as ET
try:
root=ET.parse(sys.argv[1]).getroot()
print('shutdown port:', root.get('port'))
for node in root.iter('Connector'):
print('connector:', {k: node.get(k) for k in
('port','protocol','SSLEnabled','server','connectionTimeout')})
for node in root.iter('Host'):
print('host:', {k: node.get(k) for k in
('name','autoDeploy','deployXML','deployOnStartup')})
except (OSError, ET.ParseError) as exc:
sys.exit(f'Could not read XML: {exc}')
PY_AUDIT
printf '\nAudit complete; no changes made.\n'
chmod +x tomcat-audit.sh
CATALINA_BASE=/opt/tomcat TOMCAT_SERVICE=tomcat ./tomcat-audit.sh | tee audit-local.txt
# Simpan audit-local.txt hanya di repositori/penyimpanan internal yang aman.
Hal yang wajib direview dari hasil audit
- JDK dan Tomcat benar-benar versi yang didukung; ada patch security tertunda atau tidak.
- Port yang listening sesuai desain, termasuk AJP, JMX, debug dan shutdown.
- User service bukan root dan tidak punya write access berlebihan ke konfigurasi.
- Deployment otomatis,
Manager,host-manager,examples, dandocsmemang diperlukan atau tidak. - Log tidak mengandung access token, session ID, kredensial, atau detail internal berlebihan.
2. Memahami perubahan di server.xml dan context.xml
Di bawah ini contoh struktur XML, bukan file server.xml lengkap. Jangan replace file asli: listener, TLS, keystore, realm, connector dan virtual host yang ada dapat hilang. Pada lingkungan reverse proxy lokal, connector dapat di-bind ke 127.0.0.1; untuk proxy di mesin lain gunakan IP antarmuka internal yang sesuai.
<!-- Illustrative excerpt; merge into existing server.xml, never replace whole file. -->
<Server port="-1">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
address="127.0.0.1"
connectionTimeout="20000"
server="WebServer"
rejectIllegalHeader="true" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
autoDeploy="false" deployXML="false">
<Valve className="org.apache.catalina.valves.ErrorReportValve"
showReport="false" showServerInfo="false" />
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs" prefix="access_log" suffix=".txt"
pattern="%a %t "%r" %s %b %D" />
</Host>
</Engine>
</Service>
</Server>
Menonaktifkan shutdown port berguna jika proses dihentikan oleh service manager yang benar. Namun, bila unit systemd masih mengandalkan shutdown.sh untuk stop, perubahan ini bisa membuat penghentian layanan tidak berjalan sesuai harapan. Parameter itu tidak dimasukkan dalam skrip otomatis di bawah.
<!-- conf/context.xml: only after reviewing all applications -->
<Context useHttpOnly="true" crossContext="false" privileged="false">
<!-- Set sessionCookiePath, SameSite, or secure flag only after testing
HTTPS offload, authentication and browser behavior. -->
</Context>
Kenapa tidak mengunci semua cookie dan header secara otomatis?
Aplikasi dengan autentikasi lintas domain, SSO, WebSocket, redirect HTTPS, atau reverse proxy punya karakteristik berbeda. HttpOnly biasanya layak, sedangkan Secure dan SameSite harus divalidasi pada alur login dan cross-site request. Header HSTS dan CSP paling mudah dikelola di edge, tetapi butuh keputusan scope dan pengecualian yang jelas.
3. Remediasi berbasis Python: tampilkan diff, baru apply
Ini bagian yang paling berguna saat menangani banyak instance dengan pola konfigurasi serupa. Skrip sengaja dibatasi: memilih satu port connector, memperbarui banner/error valve, dan menonaktifkan hot deployment pada host. Konfigurasi TLS, backend, aplikasi, JMX, AJP, dan shutdown port tidak disentuh. Tidak ada auto-restart.
lxml. Uji dahulu di staging. Backup dibuat otomatis sebelum perubahan permanen, dan mode standar hanya mencetak unified diff. Jika ada beberapa Connector memakai nomor port sama atau XML tidak valid, skrip berhenti.#!/usr/bin/env python3
"""Minimal, conservative server.xml remediator. Dry-run by default.
Requires Python 3 and lxml (python3 -m pip install lxml).
Use only on a maintenance/test copy before production.
"""
import argparse
import difflib
import os
from pathlib import Path
import shutil
import sys
from datetime import datetime, timezone
from lxml import etree
parser = argparse.ArgumentParser()
parser.add_argument('--base', default='/opt/tomcat')
parser.add_argument('--port', required=True, help='Existing Connector port to target')
parser.add_argument('--header', default='WebServer')
parser.add_argument('--apply', action='store_true', help='Write changes; default is preview')
a = parser.parse_args()
file = Path(a.base) / 'conf' / 'server.xml'
if not file.is_file():
sys.exit(f'File not found: {file}')
original = file.read_bytes()
xml_parser = etree.XMLParser(resolve_entities=False, no_network=True,
remove_blank_text=False, strip_cdata=False)
root = etree.fromstring(original, parser=xml_parser)
if root.tag != 'Server':
sys.exit('Expected Server root element')
connectors = [c for c in root.iter('Connector') if c.get('port') == a.port]
if len(connectors) != 1:
sys.exit(f'Expected exactly one Connector port={a.port}, got {len(connectors)}')
conn = connectors[0]
# Apply ONLY explicitly reviewed low-risk settings to the chosen connector.
conn.set('server', a.header)
conn.set('serverRemoveAppProvidedValues', 'true')
conn.set('rejectIllegalHeader', 'true')
conn.set('connectionTimeout', '20000')
# Do not touch TLS, HTTP/2, AJP, proxy settings, apps, or shutdown port.
for host in root.iter('Host'):
host.set('deployXML', 'false')
host.set('autoDeploy', 'false')
# Avoid forcibly changing deployOnStartup: may prevent existing app startup.
valves = [v for v in host.findall('Valve')
if v.get('className') == 'org.apache.catalina.valves.ErrorReportValve']
if len(valves) > 1:
sys.exit('Multiple ErrorReportValve entries: review manually')
valve = valves[0] if valves else etree.SubElement(host, 'Valve')
valve.set('className', 'org.apache.catalina.valves.ErrorReportValve')
valve.set('showServerInfo', 'false')
valve.set('showReport', 'false')
new = etree.tostring(root, encoding='UTF-8', xml_declaration=True)
old_text = original.decode('utf-8').splitlines(keepends=True)
new_text = new.decode('utf-8').splitlines(keepends=True)
print(''.join(difflib.unified_diff(old_text, new_text,
fromfile='server.xml.before',
tofile='server.xml.after')), end='')
if not a.apply:
print('\nDRY-RUN: nothing written. Inspect diff; then rerun with --apply.')
sys.exit(0)
backup = file.with_name('server.xml.bak.' +
datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ'))
shutil.copy2(file, backup)
os.chmod(backup, 0o600)
tmp = file.with_name('.server.xml.hardening.tmp')
try:
with tmp.open('xb') as out:
out.write(new)
out.flush()
os.fsync(out.fileno())
shutil.copymode(file, tmp)
shutil.chown(tmp, user=file.stat().st_uid, group=file.stat().st_gid)
os.replace(tmp, file)
except Exception:
tmp.unlink(missing_ok=True)
raise
print(f'APPLIED. Backup: {backup}')
print('NEXT: validate with catalina.sh configtest; restart only in change window.')
# Dependency di virtualenv/admin workstation; contoh saja
python3 -m venv /tmp/tomcat-xml-venv
/tmp/tomcat-xml-venv/bin/pip install lxml
# Preview: tanpa perubahan pada server.xml
sudo /tmp/tomcat-xml-venv/bin/python tomcat-remediate.py \
--base /opt/tomcat --port 8080
# Setelah review diff, backup eksternal dan persetujuan change:
sudo /tmp/tomcat-xml-venv/bin/python tomcat-remediate.py \
--base /opt/tomcat --port 8080 --apply
# Jalankan configtest sebelum restart layanan
sudo /opt/tomcat/bin/catalina.sh configtest
Apa saja yang sengaja tidak diotomatisasi?
Rotasi dan masking log, security header, pembatasan request body, konfigurasi mTLS, enable/disable HTTP/2, penghapusan webapps, firewall, dan perubahan permission massal. Semuanya perlu audit dependensi serta validasi bisnis. Pemilihan parameter seperti maxParameterCount atau maxHttpHeaderSize sebaiknya mengikuti profil request aplikasi, bukan angka acak yang diambil dari internet.
4. Hardening host dan isolasi layanan dengan systemd
Lapisan OS sering lebih menentukan daripada menyamarkan header. Service account khusus, folder konfigurasi non-writable, limit akses outbound, dan unit sandbox memberi pengurangan risiko yang nyata. Contoh berikut template, bukan perintah untuk langsung diterapkan.
# /etc/systemd/system/tomcat.service.d/hardening.conf
# Example only: adjust writable paths & service name to your package/layout.
[Service]
User=tomcat
Group=tomcat
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/tomcat/logs /opt/tomcat/temp /opt/tomcat/work
# Do not blindly enable DynamicUser, MemoryDenyWriteExecute or
# SystemCallFilter: Java/native libraries may require extra permissions.
sudo systemd-analyze security tomcat.service
sudo systemctl cat tomcat.service
# Jika sudah menyesuaikan dan menyimpan drop-in:
sudo systemctl daemon-reload
sudo systemctl restart tomcat
sudo journalctl -u tomcat -n 120 --no-pager
Dengan ProtectSystem=strict, folder di luar ReadWritePaths menjadi tidak bisa ditulis oleh unit. Tambahkan path yang benar-benar dibutuhkan, bukan membuka seluruh /opt/tomcat. Beberapa paket atau framework juga membuat file dinamis di lokasi lain; ini alasan staging test harus mencakup startup, transaksi, unggahan dan cleanup.
5. Verifikasi, evidence, dan rollback
Hasil hardening yang bisa diterima bukan sekadar proses menunjukkan status active. Saya cek sintaks XML, port, response header, error page, log, endpoint fungsional, dan perilaku aplikasi setelah restart. Buat perbandingan sebelum/sesudah dan catat change ID pada evidence internal.
#!/usr/bin/env bash
set -Eeuo pipefail
BASE="${CATALINA_BASE:-/opt/tomcat}"
SERVICE="${TOMCAT_SERVICE:-tomcat}"
TARGET="${TARGET_URL:-http://127.0.0.1:8080}"
"$BASE/bin/catalina.sh" configtest
systemctl is-active --quiet "$SERVICE" && echo 'Service active'
curl --silent --show-error --max-time 10 -D - -o /dev/null "$TARGET/" \
| sed -n '1,18p'
curl --silent --show-error --max-time 10 -D - -o /dev/null \
"$TARGET/this-page-should-not-exist-2026" | sed -n '1,18p'
echo 'Validate TLS with openssl s_client (if TLS terminates on Tomcat).'
echo 'Confirm expected endpoints, sessions, file upload, websocket, async flows.'
Rollback harus sudah siap sebelum change
# Replace with the EXACT backup path printed by the remediator.
sudo systemctl stop tomcat
sudo cp -a /opt/tomcat/conf/server.xml.bak.YYYYMMDDTHHMMSSZ \
/opt/tomcat/conf/server.xml
sudo /opt/tomcat/bin/catalina.sh configtest
sudo systemctl start tomcat
sudo systemctl status tomcat --no-pager
# Restore the previous systemd drop-in separately if one was changed.
Checklist untuk menyatakan perubahan selesai
- Versi software dan advisory security diperiksa.
- Backup file/VM dan prosedur restore diuji atau diverifikasi.
- Diff direview dan disetujui.
- Service startup, health check dan transaksi utama lulus.
- Header, error page, connector, log dan permission dicek ulang.
- Monitoring tidak menunjukkan lonjakan error/latency.
- Rollback path dan change record terdokumentasi.
Catatan khusus: Security Manager, TLS, dan patch
Java Security Manager bukan switch yang sebaiknya diaktifkan asal
Java Security Manager historisnya menawarkan sandbox per permission melalui catalina.policy. Namun dependency lama, akses property, logging, classloader dan library native dapat terkena AccessControlException. Dukungan Java juga berubah menurut versi JDK. Untuk implementasi modern, utamakan runtime user non-root, least privilege, segmentasi jaringan, isolasi proses dan patching. Kalau lingkungan legacy masih memerlukannya, kerjakan sebagai proyek kompatibilitas terpisah dengan uji regresi menyeluruh.
TLS dan mutual TLS
Server certificate untuk serverAuth dan client certificate untuk clientAuth menjalankan peran berbeda. mTLS membutuhkan CA yang dipercaya, kebijakan validasi klien dan distribusi sertifikat yang rapi. Jika terminasi TLS ada di reverse proxy, validasi sertifikat klien dilakukan di sana; jika langsung di Tomcat, gunakan konfigurasi connector SSLHostConfig yang sesuai. Hindari meneruskan identitas sertifikat melalui header yang dapat dipalsukan.
Prioritaskan update keamanan
Ketika tulisan ini disusun (8 November 2026), advisory Tomcat 9 mencantumkan perbaikan pada 9.0.123, termasuk isu HTTP/2 dan autentikasi tertentu yang memengaruhi versi sebelumnya. Jangan mengandalkan tutorial ini sebagai bukti bahwa sebuah versi sudah bebas CVE: rujuk advisory terbaru sebelum maintenance.
Referensi teknis
- Apache Tomcat — Security Considerations
- Apache Tomcat 9 — Security Advisories
- Apache Tomcat — HTTP Connector
- Apache Tomcat — Valve Configuration
- Apache Tomcat — Security Manager (legacy)
Artikel teknis, bukan baseline universal. Sesuaikan dengan platform, versi JDK, SLA, arsitektur dan kebijakan change management masing-masing. Contoh menggunakan data sintetis.
/* Copy button stays scoped to this article; no third-party JavaScript dependency. */
(function(){
var root=document.getElementById(‘zt-tomcat-hardening’);
if(!root) return;
root.querySelectorAll(‘.zt-copy’).forEach(function(button){
button.addEventListener(‘click’,async function(){
var source=button.closest(‘.zt-code’).querySelector(‘.zt-source’);
var value=source ? source.textContent : ”;
try{await navigator.clipboard.writeText(value);button.textContent=’Tersalin’;}
catch(e){button.textContent=’Blok kode lalu salin’;}
setTimeout(function(){button.textContent=’Salin kode’;},1800);
});
});
})();