Web interface to view, ban and unban fail2ban IPs on several hosts via SSH, generalised from the private f2b-web tool. - Web UI (Flask/Waitress): hosts.yaml, local/OIDC/proxy authentication, English/German UI, origin map, optional Loki/Grafana/GeoIP, CLI helpers - Agent f2b-webctl with install.sh/uninstall.sh (dedicated user + sudo, from= restriction, protected networks, manual jail) - Hardened Docker image, examples, documentation, smoke test, testbed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.2 KiB
HTML
80 lines
3.2 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block header_meta %}
|
||
<span id="generated">–</span>
|
||
<button id="refresh" type="button">{{ t('refresh') }}</button>
|
||
{% if client_config.grafana and client_config.grafanaDashboard %}<a class="btn-link" href="{{ client_config.grafana }}{{ client_config.grafanaDashboard }}" target="_blank" rel="noopener">{{ t('grafana') }}</a>{% endif %}
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<main>
|
||
<section id="hosts" class="cards"></section>
|
||
|
||
{% if geo_enabled %}
|
||
<section class="panel map-panel">
|
||
<div class="toolbar">
|
||
<h2>{{ t('map_heading') }}</h2>
|
||
<span id="map-summary" class="muted"></span>
|
||
<button id="map-reset" type="button" class="small" hidden>{{ t('map_reset') }}</button>
|
||
</div>
|
||
<div class="map">
|
||
<svg id="map" viewBox="0 5 360 145" role="img" aria-label="{{ t('map_heading') }}">
|
||
<image href="{{ url_for('static', filename='world.svg') }}" x="0" y="5" width="360" height="145"/>
|
||
<g id="map-points"></g>
|
||
</svg>
|
||
</div>
|
||
<p class="hint">{{ t('map_hint') }}</p>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<section class="panel">
|
||
<h2>{{ t('ban_heading') }}</h2>
|
||
<form id="ban-form" autocomplete="off">
|
||
<input id="ban-ip" type="text" placeholder="{{ t('ban_ip_placeholder') }}" required>
|
||
<input id="ban-note" type="text" placeholder="{{ t('ban_note_placeholder') }}" maxlength="200">
|
||
<span class="hosts-select">
|
||
{% for h in hosts %}<label><input type="checkbox" name="ban-host" value="{{ h }}" checked> {{ h }}</label>{% endfor %}
|
||
</span>
|
||
<button type="submit" class="danger">{{ t('ban_button') }}</button>
|
||
</form>
|
||
<p class="hint">{{ t('ban_hint', jail=manual_jail) }}</p>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<div class="toolbar">
|
||
<h2>{{ t('bans_heading') }} <span id="count" class="badge">0</span></h2>
|
||
<input id="filter-text" type="search" placeholder="{{ t('filter_search') }}">
|
||
<select id="filter-host"><option value="">{{ t('filter_all_hosts') }}</option></select>
|
||
<select id="filter-jail"><option value="">{{ t('filter_all_jails') }}</option></select>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>{{ t('col_host') }}</th><th>{{ t('col_jail') }}</th><th>{{ t('col_ip') }}</th><th>{{ t('col_origin') }}</th><th>{{ t('col_since') }}</th><th>{{ t('col_until') }}</th><th>{{ t('col_actions') }}</th></tr>
|
||
</thead>
|
||
<tbody id="bans"><tr><td colspan="7" class="muted">{{ t('loading') }}</td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>{{ t('audit_heading') }}</h2>
|
||
<ul id="audit" class="audit"></ul>
|
||
</section>
|
||
</main>
|
||
|
||
<dialog id="history">
|
||
<div class="dialog-head">
|
||
<h2 id="history-title"></h2>
|
||
<button type="button" id="history-close" aria-label="{{ t('history_close') }}">✕</button>
|
||
</div>
|
||
<div id="history-body"></div>
|
||
</dialog>
|
||
|
||
<div id="toast" role="status" aria-live="polite"></div>
|
||
<script id="i18n" type="application/json">{{ i18n | tojson }}</script>
|
||
<script id="client-config" type="application/json">{{ client_config | tojson }}</script>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}<script src="{{ url_for('static', filename='app.js') }}"></script>{% endblock %}
|