68 Commits

Author SHA1 Message Date
9a0cf7bd8a Merge pull request 'v0.5.0' (#5) from develop into main
Reviewed-on: #5
2026-01-17 20:25:14 +05:00
b938b73cfd Update CHANGELOG.md with 0.5.0 release date 2026-01-17 20:15:43 +05:00
ce031be060 Update CHANGELOG.md with sudo login tracking and notification details 2026-01-15 00:31:44 +05:00
5e50bc179f Add sudo command login tracking and notification support 2026-01-15 00:28:11 +05:00
279f58b644 Update CHANGELOG.md with su login tracking and notification details 2026-01-14 23:27:52 +05:00
26365a519b Add su command login tracking and notification support 2026-01-14 23:25:16 +05:00
d1f307d2ad Update CHANGELOG.md with 0.5.0 changes: add local login tracking and notifications 2026-01-14 21:51:55 +05:00
ccf228242d Add TTY login tracking with notification support 2026-01-14 21:51:20 +05:00
5e12b1f6ab Refactor: Rename SSH alert keys for clarity and update relevant usages 2026-01-13 22:09:42 +05:00
67abcc0ef2 Refactor: Rename processLogin to process in SSH analyzer for consistency 2026-01-13 00:27:11 +05:00
5ad40cdf9b Refactor: Rename process to processLogin in SSH analyzer for clarity 2026-01-13 00:24:07 +05:00
374abcea80 Refactor: Consolidate sshProcessReturn into generic processReturn for improved reusability 2026-01-13 00:18:55 +05:00
4748630b04 Merge pull request 'v0.4.0' (#4) from develop into main
Reviewed-on: #4
2026-01-11 17:01:42 +05:00
a75df70922 Update CHANGELOG.md with release date for version 0.4.0 2026-01-11 16:50:56 +05:00
a84f1ccde6 Update CHANGELOG.md to document IP blocking fix during Docker container redirection 2026-01-11 16:49:58 +05:00
0d13f851dd Fixed a bug where IP blocking for containers did not work when Docker was enabled 2026-01-11 16:44:33 +05:00
b04016c596 Update CHANGELOG.md to include rule_strategy parameter addition and its configuration details 2026-01-11 15:58:25 +05:00
8147e715f2 Update default rule_strategy to incremental and handle new strategy in Docker settings 2026-01-11 15:52:17 +05:00
f57172a2ea Add IncrementalStrategy for rule management and extend chain functionality to support rule listing and removal 2026-01-11 15:51:54 +05:00
6c5a476d6e Refactor bridge name generation and extend IPInfo with NetworkID for improved modularity and network tracking 2026-01-11 14:21:43 +05:00
264f8ac60b Add NetworkID field to IPInfo and Docker network settings for enhanced network tracking 2026-01-11 14:21:20 +05:00
b2a9f83a44 Add FetchContainer method to Docker client for improved modularity and encapsulation 2026-01-11 13:29:30 +05:00
6ac0a86d9d Add FetchBridge method to Docker client for improved encapsulation and modularity 2026-01-11 13:26:34 +05:00
a6133c308e Refactor Docker client methods: bridge and container helper methods for improved encapsulation and naming consistency 2026-01-11 12:57:48 +05:00
82b501d0ec Refactor rule generation: add GenerateBridge and GenerateContainer methods for improved modularity and clarity 2026-01-11 12:54:38 +05:00
ce6cbbe17e Add optional comment parameter to JumpTo for enhanced rule traceability 2026-01-11 00:47:38 +05:00
2de8aa29c4 Update rebuildStrategy to handle container-specific events and adjust GenerateAll calls with an extra parameter 2026-01-11 00:46:19 +05:00
3afd4aa5f3 Add optional comment support in rule generation for improved traceability 2026-01-11 00:45:47 +05:00
42160ff5ab Enhance Docker event monitoring: add support for network events, JSON unmarshaling for events, and include detailed event attributes 2026-01-11 00:44:58 +05:00
8798811806 Refactor Docker bridge handling: consolidate bridge name and subnet methods into BridgeInfo for simplified logic and enhanced structure 2026-01-11 00:23:49 +05:00
a10d56df79 Add --no-trunc to Docker ps command for full container IDs in monitoring 2026-01-10 23:49:12 +05:00
876592c38d Refactor RebuildStrategy: rename to rebuildStrategy for improved naming consistency and update method receivers accordingly 2026-01-10 21:40:33 +05:00
e55660b098 Introduce rule management strategies and refactor Docker monitoring logic 2026-01-09 23:45:57 +05:00
c6c3f991cc Update CHANGELOG.md with details on options.docker_support removal and related Docker configuration changes 2026-01-07 20:29:20 +05:00
bc177f83b8 Add support for Docker configuration and refactor related settings 2026-01-07 20:28:54 +05:00
48be913c57 Refactor analyzer: replace slice initialization with var keyword for clarity 2026-01-07 20:27:55 +05:00
0a30733d27 When the program stops, there are cases when empty messages "Received log entry" appear in the logs 2026-01-07 20:27:28 +05:00
4a5492b1c5 Add check to skip empty Docker event messages in monitoring loop 2026-01-05 22:48:20 +05:00
a3df113b07 Update CHANGELOG.md with details on fixes related to binaryLocations.docker setting and Docker crash issue 2026-01-05 22:46:07 +05:00
e034debeaa Refactor Docker event monitoring: simplify Events method and introduce EventsClose for graceful shutdowns 2026-01-05 22:41:30 +05:00
9134ab8ec0 Refactor systemd log watcher to build args within the watch method 2026-01-05 22:33:52 +05:00
ba23474eab Refactor analyzer to use a shared log channel 2026-01-05 22:27:45 +05:00
bbaf0304c3 Merge pull request 'v0.3.0' (#3) from develop into main
Reviewed-on: #3
2026-01-04 17:09:39 +05:00
1f8be77ab3 Clarify Docker support status in English README 2026-01-04 16:39:21 +05:00
d2795639da Update Russian README: reorder sections and clarify Docker support status 2026-01-04 16:39:08 +05:00
8638c49886 Add "Requirements" section to English README 2026-01-04 16:37:16 +05:00
66e6bad111 Add system requirements section to README 2026-01-04 16:37:06 +05:00
1a6d6b813b Update CHANGELOG.md with release date for version 0.3.0 2026-01-04 16:36:36 +05:00
9b8d07ccb3 Fix typo in CHANGELOG.md: correct WantedBy target from sysinit.target to multi-user.target 2026-01-04 16:20:05 +05:00
4b8622a870 Update CHANGELOG.md with partial Docker support details for version 0.3.0 2026-01-04 16:19:30 +05:00
b9719f7eaf Add Docker event monitoring and chain clearing functionality
- Introduced `Events` method in Docker client to stream and handle Docker events.
- Added `Clear` method to nftables chain interface for clearing rules.
- Enhanced daemon lifecycle to include Docker event monitoring when Docker support is enabled.
- Updated nftables rule management with event-driven chain clearing and reloading.
2026-01-04 16:06:01 +05:00
c424621615 Add Docker support with nftables integration
- Introduced Docker monitoring to manage nftables rules.
- Added `docker_support` option to firewall configuration.
- Integrated Docker bridge, container handling, and related network rules.
- Updated default configurations for Docker path and settings.
- Enhanced `daemon` lifecycle for Docker integration.
2026-01-04 13:59:26 +05:00
865f12d966 Update dependencies: bump go-nftables-client to v0.1.1 and make go-mail a direct dependency 2026-01-01 22:06:50 +05:00
b3a94855b8 Refactor localOutput receiver names for consistency in AddRule and AddRuleOut methods 2026-01-01 20:28:54 +05:00
4d001a026c Refactor localInput receiver names for consistency in AddRule and AddRuleIn methods 2026-01-01 20:28:37 +05:00
6e4bd17bfe Update CHANGELOG.md to include new configuration files notifications.toml and analyzer.toml 2025-12-31 23:14:09 +05:00
0bcdb7bcc7 Update LICENSE-3RD-PARTY.txt to include go-mail dependency and its MIT license details 2025-12-31 23:05:56 +05:00
5f2d5a1a9e Simplify EmptyAnalysis.Process by ignoring unused parameter 2025-12-31 23:01:20 +05:00
542f7415b7 Update CHANGELOG.md with email notification and SSH login notification details for version 0.3.0 2025-12-31 22:58:25 +05:00
8615c79f12 Refactor log analyzer to support SSH login detection
- Moved `Entry` type to `analysis` package for better organization.
- Introduced `SSH` analysis service to detect and notify about SSH logins.
- Added notification and logging for detected SSH login events.
2025-12-31 22:52:12 +05:00
b5686a2ee6 Add systemd log integration for analyzer service
- Implemented `systemd` log monitoring using `journalctl`.
- Added `BinPath` configuration for specifying binary paths.
- Introduced `ssh` unit monitoring for authorization tracking.
- Updated analyzer lifecycle to integrate log processing.
- Enhanced validation for `journalctl` path in settings.
- Updated default configurations with `journalctl` path.
2025-12-30 20:57:35 +05:00
e78685c130 Add support for analyzer service and configuration
- Introduced `analyzer` service for log parsing and authorization tracking.
- Added dedicated analyzer configuration via `analyzer.toml`.
- Integrated analyzer setup and lifecycle management into daemon runtime.
- Enhanced `setting` package to include analyzer settings parsing and validation.
- Updated daemon options to support analyzer configuration.
- Extended default configuration files for analyzer settings.
2025-12-30 15:03:41 +05:00
74dce294bf Add support for email notifications
- Introduced email notifications enabling configuration via `notifications.toml`.
- Created notification handling within `internal/daemon/notifications`.
- Added async email queue with error handling and customizable TLS configurations.
- Integrated notifications setup and validation into the daemon runtime.
2025-12-16 19:30:18 +05:00
6929ac9bf5 Update systemd service file for kor-elf-shield to improve reliability
- Added `Restart=on-failure` with a 10-second delay.
- Changed `WantedBy` target to `multi-user.target`.
- Defined service type as `simple`.
2025-12-08 23:19:38 +05:00
69157c90cb Merge pull request 'v0.2.0' (#2) from develop into main
Reviewed-on: #2
2025-11-29 16:12:03 +05:00
7054efd359 Update CHANGELOG.md with release date for version 0.2.0 2025-11-29 15:41:12 +05:00
57948fb639 Add support for chain priority configuration in nftables
- Introduced `input_priority`, `output_priority`, and `forward_priority` options in `firewall.toml`.
- Updated `chains` and chain creation functions to include priority handling.
- Added validation for priority values to ensure they remain within the acceptable range (-50 to 50).
- Adjusted `reloadInput`, `reloadOutput`, and `reloadForward` to respect priority settings.
2025-11-29 15:38:58 +05:00
6e7b6093f1 Add support for clear_mode option to toggle nftables clearing behavior
- Introduced `clear_mode` parameter in `firewall.toml` with options for clearing all nftables rules (`global`) or table-specific rules (`own`).
- Updated `chains` and `firewall` logic to respect `clear_mode` configuration.
- Enhanced `options` parsing and validation for `clear_mode`.
- Updated `CHANGELOG.md` to reflect the addition of `clear_mode`.
2025-11-25 20:58:12 +05:00
73 changed files with 3790 additions and 70 deletions

View File

@@ -1,3 +1,90 @@
## 0.5.0 (17.1.2026)
***
#### Русский
* В настройках analyzer.toml добавил параметры local_enable и local_notify.
* local_enable = Включает отслеживание локальных авторизаций (TTY, физический доступ). По умолчанию включён.
* local_notify = Включает уведомления о локальных авторизациях. По умолчанию включён.
* В настройках analyzer.toml добавил параметры su_enable и su_notify.
* su_enable = Включает отслеживание авторизаций через su. По умолчанию включён.
* su_notify = Включает уведомления об авторизациях через su. По умолчанию включён.
* В настройках analyzer.toml добавил параметры sudo_enable и sudo_notify.
* sudo_enable = Включает отслеживание авторизаций через sudo. По умолчанию выключен.
* sudo_notify = Включает уведомления об авторизациях через sudo. По умолчанию включён.
***
#### English
* Added local_enable and local_notify parameters to analyzer.toml settings.
* local_enable = Enables tracking of local logins (TTY, physical access). Enabled by default.
* local_notify = Enables notifications about local logins. Enabled by default.
* Added su_enable and su_notify parameters to analyzer.toml settings.
* su_enable = Enables tracking of logins via su. Enabled by default.
* su_notify = Enables notifications about logins via su. Enabled by default.
* Added sudo_enable and sudo_notify parameters to analyzer.toml settings.
* sudo_enable = Enables tracking of logins via sudo. Off by default.
* sudo_notify = Enables notifications about logins via sudo. Enabled by default.
***
## 0.4.0 (11.1.2026)
***
#### Русский
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
* В настройках docker.toml добавил возможность переключать режим работы с Docker через параметр rule_strategy.
* incremental = добавляются или удаляются только правила конкретного контейнера (сейчас по умолчанию)
* rebuild = при любом изменении все цепочки Docker пересоздаются целиком (старый режим)
* Исправлена ошибка:
* Настройка binaryLocations.docker не работала.
* Программа аварийно завершалась после остановки Docker'а.
* Указанные в настройках IP-адреса не блокировались во время перенаправления в контейнер Docker.
***
#### English
* Removed the options.docker_support parameter from firewall.toml. Docker settings have been moved to the docker.toml file.
* Added the ability to switch Docker operation mode via the rule_strategy parameter to the docker.toml settings.
* incremental = only rules for a specific container are added or removed (currently the default)
* rebuild = any change rebuilds all Docker chains (old mode)
* Fixed error:
* The binaryLocations.docker setting did not work.
* The program crashed after Docker was stopped.
* The IP addresses specified in the settings were not blocked during redirection to the Docker container.
***
## 0.3.0 (4.1.2026)
***
#### Русский
* Добавлена частичная поддержка Docker.
* Добавлен параметр options.docker_support в firewall.toml. Это включает поддержку Docker.
* Каждый запуск контейнера будет полностью пересчитываться правила у chain, которые относятся к Docker. (в будущем планирую это переработать)
* Добавлены настройки для уведомлений по электронной почте.
* Добавлен файл настроек notifications.toml.
* Реализовано уведомление о входах по SSH.
* Добавлен файл настроек analyzer.toml.
* Служба systemd
* Изменено WantedBy с sysinit.target на multi-user.target
* Убрано ExecStop. По факту это не работало. Чтобы остановить сервис с очисткой правил nftables выпоните команду: kor-elf-shield stop
* Добавлено Restart=on-failure. Нужно для того, чтобы программа перезапустилась после критической ошибки.
***
#### English
* Added partial Docker support.
* Added the options.docker_support parameter to firewall.toml. This enables Docker support.
* Each container launch will completely recalculate the Docker-specific rules in chain. (I plan to rework this in the future)
* Added settings for email notifications.
* Added notifications.toml settings file.
* Implemented notification of SSH logins.
* Added analyzer.toml settings file.
* Systemd service
* Changed WantedBy from sysinit.target to multi-user.target
* Removed ExecStop. It didn't actually work. To stop the service and clear the nftables rules, run the command: kor-elf-shield stop
* Added Restart=on-failure. This is necessary to ensure the program restarts after a critical error.
## 0.2.0 (29.11.2025)
***
#### Русский
* Добавлен параметр clear_mode в firewall.toml. Он позволяет переключать режим очистки всех правил в nftables или только таблицу относящие к программе.
* Добавлен параметр input_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain input.
* Добавлен параметр output_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain output.
* Добавлен параметр forward_priority в firewall.toml. Можно указать приоритет от -50 по 50 к chain forward.
***
#### English
* Added the clear_mode parameter to firewall.toml. It allows you to toggle clearing of all rules in nftables or only the program-specific table.
* Added the input_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain input.
* Added the output_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain output.
* Added the forward_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain forward.
***
## 0.1.0 (8.11.2025)
***
#### Русский

View File

@@ -684,6 +684,32 @@ SOFTWARE.
--------------------------------------------------------------------------------
github.com/wneessen/go-mail
MIT License
Copyright (c) 2022-2025 The go-mail Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
go.uber.org/multierr
Copyright (c) 2017-2021 Uber Technologies, Inc.

View File

@@ -6,6 +6,15 @@
***
<p style="color: red; font-weight: bold; font-size: 20px;">Требования:</p>
* Запуск от имени root
* Linux 5.2+
* nftables
* Systemd
***
### Сделанно:
* Реализована возможность настраивать nftables:
* По умолчанию разрешить или блокировать входящий трафик.
@@ -14,11 +23,11 @@
* Настройка портов.
* Настройка белых и чёрных списков IP адресов.
* Настройка логирование.
### В планах:
* Подружить с docker.
* Подружить с docker (частично).
* Внедрить настройку уведомлений (пока только e-mail).
* Отправлять уведомления при авторизации ssh.
### В планах:
* Защита от перебора паролей (brute-force).
* Уведомлять, если появится новый пользователь в системе.
* Уведомлять, если изменились системные файлы.

View File

@@ -6,6 +6,15 @@
***
<p style="color: red; font-weight: bold; font-size: 20px;">Requirements:</p>
* Run as root
* Linux 5.2+
* nftables
* Systemd
***
### Done:
* The ability to configure nftables has been implemented:
* Allow or block incoming traffic by default.
@@ -14,11 +23,11 @@
* Port configuration.
* Setting up white and black lists of IP addresses.
* Setting up logging.
### The plans include:
* Make friends with docker.
* Make friends with docker (partially).
* Implement notification settings (for now only by e-mail).
* Send notifications during ssh authorization.
### The plans include:
* Password brute-force protection.
* Notify if a new user appears in the system.
* Notify if system files have changed.

View File

@@ -0,0 +1,110 @@
###############################################################################
# РАЗДЕЛ:Отслеживать авторизаций
# ***
# SECTION:Track authorizations
###############################################################################
[login]
###
# Включает группу отслеживания авторизации.
# Если отключено, отслеживание авторизации работать не будет.
# По умолчанию: true
# ***
# Enables the authorization tracking group.
# If disabled, no authorization tracking will work.
# Default: true
###
enabled = true
###
# Включает уведомления об авторизации.
# Если отключено, они будут отображаться в логах только на уровне = "info".
# По умолчанию: true
# ***
# Enables authorization notifications.
# If disabled, they will only appear in the logs under level = "info".
# Default: true
###
notify = true
###
# Включает отслеживание авторизации по ssh.
# По умолчанию: true
# ***
# Enables tracking of SSH authorization.
# Default: true
###
ssh_enable = true
###
# Включает уведомления об авторизации по ssh.
# Если отключено, они будут отображаться в логах только на уровне = "info".
# По умолчанию: true
# ***
# Enables SSH authorization notifications.
# If disabled, they will only appear in the logs under level = "info".
# Default: true
###
ssh_notify = true
###
# Включает отслеживание локальных авторизаций (TTY, физический доступ).
# По умолчанию: true
# ***
# Enables tracking of local authorizations (TTY, physical access).
# Default: true
###
local_enable = true
###
# Включает уведомления о локальных авторизациях.
# По умолчанию: true
# ***
# Enables local authorization notifications.
# Default: true
###
local_notify = true
###
# Включает отслеживание, если кто-либо использует команду `su` для доступа к другой учетной записи.
# По умолчанию: true
# ***
# Enables tracking if someone uses the `su` command to access another account.
# Default: true
###
su_enable = true
###
# Включает уведомления, если кто-либо использует команду `su` для доступа к другой учетной записи.
# По умолчанию: true
# ***
# Enables notifications if someone uses the `su` command to access another account.
# Default: true
###
su_notify = true
###
# Включает отслеживание, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
#
# ПРИМЕЧАНИЕ: Эта опция может стать обременительной, если команда sudo широко используется
# для получения root-доступа администраторами или панелями управления.
#
# По умолчанию: false
# ***
# Enables tracking if someone uses the `sudo` command to access another account.
#
# NOTE: This option could become onerous if sudo is used extensively for root
# access by administrators or control panels.
#
# Default: false
###
sudo_enable = false
###
# Включает уведомления, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
# По умолчанию: true
# ***
# Enables notifications if someone uses the `sudo` command to access another account.
# Default: true
###
sudo_notify = true

View File

@@ -0,0 +1,23 @@
###
# Включает поддержку docker.
# По умолчанию: false
# ***
# Includes docker support.
# Default: false
###
enabled = false
###
# Стратегия управления правилами при запуске или остановке контейнеров в Docker:
# rebuild = при любом изменении все цепочки Docker пересоздаются целиком
# incremental = добавляются или удаляются только правила конкретного контейнера
#
# По умолчанию: "incremental"
# ***
# # Strategy for managing rules when container start or stop events occur in docker:
# rebuild = any change causes all Docker chains to be rebuilt entirely
# incremental = only rules for a specific container are added or removed
#
# Default: "incremental"
###
rule_strategy = "incremental"

View File

@@ -299,6 +299,26 @@ icmp_strict = false
# SECTION:General Settings
###############################################################################
[options]
###
# Переключения режима очистки фаервола nftables. Если указать "own", то может получиться конфликт в правилах.
# Может спровоцировать проблему в безопасности. Указывайте "own" если вы уверены в своих действиях.
# Допустимые значения:
# global = очищает полностью все правила
# own = очищает только правила от таблицы, которые указаны в параметре table_name
#
# По умолчанию: global
# ***
# Switching the nftables firewall cleaning mode. If you specify "own", a conflict in the rules may occur.
# This may cause a security issue. Use "own" if you are confident in your actions.
# Valid values:
# global = clears all rules completely
# own = clears only the rules from the table that are specified in the table_name parameter
#
# Default: global
###
clear_mode = "global"
###
# Будет ли демон сохранять правила в системный файл nftables.
# Не забудьте проверить, что путь к nftables соответствует вашей ОС.
@@ -409,6 +429,21 @@ default_allow_forward = false
###
input_drop = "drop"
###
# Приоритет chain для input.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for input.
# From: -50
# To: 50
#
# Default: -10
###
input_priority = -10
###
# Как заблокировать исходящий трафик. Блокировать молча или с обратной связью.
# Допустимые значения:
@@ -426,6 +461,21 @@ input_drop = "drop"
###
output_drop = "reject"
###
# Приоритет chain для output.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for output.
# From: -50
# To: 50
#
# Default: -10
###
output_priority = -10
###
# Как заблокировать трафик forward. Блокировать молча или с обратной связью.
# Допустимые значения:
@@ -443,6 +493,21 @@ output_drop = "reject"
###
forward_drop = "drop"
###
# Приоритет chain для forward.
# От: -50
# По: 50
#
# По умолчанию: -10
# ***
# Chain priority for forward.
# From: -50
# To: 50
#
# Default: -10
###
forward_priority = -10
###############################################################################
# РАЗДЕЛ:Именование метаданных
# ***
@@ -483,4 +548,4 @@ chain_output_name = "output"
# Chain name for forward
# Default: "forward"
###
chain_forward_name = "forward"
chain_forward_name = "forward"

View File

@@ -192,6 +192,24 @@ log_error_paths = ["stderr"]
###
nftables = "/usr/sbin/nft"
###
# Укажите путь к journalctl. Возможно в вашей ОС путь может отличаться.
# По умолчанию: /bin/journalctl
# ***
# Specify the path to journalctl. The path may differ in your OS.
# Default: /bin/journalctl
###
journalctl = "/bin/journalctl"
###
# Укажите путь к docker. Возможно в вашей ОС путь может отличаться.
# По умолчанию: /usr/bin/docker
# ***
# Specify the path to docker. The path may differ in your OS.
# Default: /usr/bin/docker
###
docker = "/usr/bin/docker"
###############################################################################
# РАЗДЕЛ:Пути к другим настройкам
# ***
@@ -210,3 +228,36 @@ nftables = "/usr/sbin/nft"
# Default: /etc/kor-elf-shield/firewall.toml
###
firewall = "/etc/kor-elf-shield/firewall.toml"
###
# Укажите путь к настройкам уведомлений.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/notifications.toml
# ***
# Specify the path to notification settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/notifications.toml
###
notifications = "/etc/kor-elf-shield/notifications.toml"
###
# Укажите путь к настройкам парсинга логов.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/analyzer.toml
# ***
# Specify the path to the log parsing settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/analyzer.toml
###
analyzer = "/etc/kor-elf-shield/analyzer.toml"
###
# Укажите путь к настройкам docker.
# Файл должен иметь расширение .toml.
# По умолчанию: /etc/kor-elf-shield/docker.toml
# ***
# Specify the path to the docker settings.
# The file must have the .toml extension.
# Default: /etc/kor-elf-shield/docker.toml
###
docker = "/etc/kor-elf-shield/docker.toml"

View File

@@ -0,0 +1,170 @@
###############################################################################
# РАЗДЕЛ:Базовые настройки
# ***
# SECTION:Basic settings
###############################################################################
###
# Включает или выключает уведомления.
# !!! Не забудьте перед включением настроить email !!!
# false = Выключает.
# true = Включает.
#
# По умолчанию: false
# ***
# Turns notifications on or off.
# !!! Don't forget to set up your email before turning it on !!!
# false = Disables.
# true = Enables.
#
# Default: false
###
enabled = false
###
# Название сервера в уведомлениях
# По умолчанию: server
# ***
# Server name in notifications
# Default: server
###
server_name = "server"
###############################################################################
# РАЗДЕЛ:email
# ***
# SECTION:email
###############################################################################
[email]
###
# Сервер, через который будет отправляться почта.
# Например: smtp.gmail.com
# По умолчанию:
# ***
# The server through which mail will be sent.
# For example: smtp.gmail.com
# Default:
###
host = ""
###
# Указать порт сервера, через который будет отправляться почта.
# Например: 587
# По умолчанию:
# ***
# Specify the server port through which mail will be sent.
# For example: 587
# Default:
###
port = ""
###
# Логин к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Login to the server through which mail will be sent.
# Default:
###
username = ""
###
# Пароль к серверу, через который будет отправляться почта.
# По умолчанию:
# ***
# Password for the server through which mail will be sent.
# Default:
###
password = ""
###
# Тип авторизации.
# Варианты: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Обычно используется "PLAIN". Если у вас внутренний релей без пароля - используйте "NONE".
# По умолчанию: "PLAIN"
# ***
# Authorization type.
# Options: "PLAIN", "LOGIN", "CRAM-MD5", "NONE"
# Usually "PLAIN" is used. If you have an internal relay without a password - use "NONE".
# Default: "PLAIN"
###
auth_type = "PLAIN"
###
# Защищённое соединение.
# Варианты: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — без TLS
# "STARTTLS" — обычный SMTP на 587 (или 25) + upgrade через STARTTLS
# "IMPLICIT" — SMTPS (TLS сразу), обычно 465
#
# По умолчанию: "STARTTLS"
# ***
# Secure connection.
# Options: "NONE", "STARTTLS", "IMPLICIT"
#
# "NONE" — without TLS
# "STARTTLS" — regular SMTP on 587 (or 25) + upgrade via STARTTLS
# "IMPLICIT" — SMTPS (TLS Immediately), typically 465
#
# Default: "STARTTLS"
###
tls_mode = "STARTTLS"
###
# Только если тип защищённого соединения в режиме starttls.
# Варианты: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — если STARTTLS недоступен/не удался будет вызвана ошибка
# "OPPORTUNISTIC" — попытаться STARTTLS, но если нельзя, то попытается отправить без TLS
#
# По умолчанию: "MANDATORY"
# ***
# Only if the secure connection type is in starttls mode.
# Options: "MANDATORY", "OPPORTUNISTIC"
#
# "MANDATORY" — if STARTTLS is unavailable/failed, an error will be raised
# "OPPORTUNISTIC" — try STARTTLS, but if that fails, it will try to send without TLS
#
# Default: "MANDATORY"
###
tls_policy = "MANDATORY"
###
# Проверять ли сертификат защищённого соединения.
#
# false = Выключает.
# true = Включает.
#
# По умолчанию: true
# ***
# Whether to check the secure connection certificate.
#
# false = Disables.
# true = Enables.
#
# Default: true
###
tls_verify = true
###
# Email, который будет указываться при отправки почты.
# Например: test@localhost
# По умолчанию:
# ***
# Email that will be specified when sending mail.
# For example: test@localhost
# Default:
###
from = ""
###
# Адрес электронной почты, на который будет отправлено письмо.
# Например: root@localhost
# По умолчанию:
# ***
# Email to whom the mail will be sent.
# For example: root@localhost
# Default:
###
to = ""

View File

@@ -3,8 +3,10 @@ Description=kor-elf-shield
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/kor-elf-shield start
ExecStop=/usr/sbin/kor-elf-shield stop
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=sysinit.target
WantedBy=multi-user.target

3
go.mod
View File

@@ -3,16 +3,17 @@ module git.kor-elf.net/kor-elf-shield/kor-elf-shield
go 1.25
require (
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/spf13/viper v1.21.0
github.com/urfave/cli/v3 v3.4.1
github.com/wneessen/go-mail v0.7.2
go.uber.org/zap v1.27.0
golang.org/x/sys v0.36.0
golang.org/x/text v0.29.0
)
require (
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect

6
go.sum
View File

@@ -1,5 +1,5 @@
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0 h1:jglai6XEk1uSCxd1TEpx6IBqWhkc+KgonV6rUDTkyyU=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.0/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1 h1:3oGtZ/r1YAdlvI16OkZSCaxcWztHe/33ITWfI2LaQm0=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -40,6 +40,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=

View File

@@ -2,8 +2,11 @@ package daemon
import (
"context"
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
@@ -34,7 +37,12 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
_ = logger.Sync()
}()
config, err := setting.Config.ToDaemonOptions()
dockerService, dockerSupport, err := newDockerService(ctx, logger)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create docker service: %s", err))
}
config, err := setting.Config.ToDaemonOptions(dockerSupport)
if err != nil {
logger.Fatal(err.Error())
@@ -43,7 +51,16 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
return err
}
d, err := daemon.NewDaemon(config, logger)
notificationsService, err := newNotificationsService(logger)
if err != nil {
logger.Fatal(err.Error())
// Fatal should call os.Exit(1), but there's a chance that might not happen,
// so we return err just in case.return err
return err
}
d, err := daemon.NewDaemon(config, logger, notificationsService, dockerService)
if err != nil {
logger.Fatal(err.Error())
@@ -63,3 +80,31 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
return nil
}
func newNotificationsService(logger log.Logger) (notifications.Notifications, error) {
config, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
if err != nil {
return nil, err
}
return notifications.New(config, logger), nil
}
func newDockerService(ctx context.Context, logger log.Logger) (dockerService docker_monitor.Docker, dockerSupport bool, err error) {
config, dockerSupport, err := setting.Config.OtherSettingsPath.ToDockerConfig(setting.Config.BinaryLocations)
if err != nil {
return docker_monitor.NewDockerNotSupport(), false, err
}
if !dockerSupport {
dockerService = docker_monitor.NewDockerNotSupport()
return dockerService, false, nil
}
dockerService, err = docker_monitor.New(&config, ctx, logger)
if err != nil {
return docker_monitor.NewDockerNotSupport(), false, err
}
return dockerService, dockerSupport, nil
}

View File

@@ -0,0 +1,115 @@
package analyzer
import (
"context"
"fmt"
config2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
analyzerLog "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Analyzer interface {
Run(ctx context.Context)
Close() error
}
type analyzer struct {
config config2.Config
logger log.Logger
notify notifications.Notifications
systemd analyzerLog.Systemd
analysis analyzerLog.Analysis
logChan chan analysisServices.Entry
}
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
var units []string
if config.Login.Enabled {
if config.Login.SSH.Enabled {
units = append(units, "_SYSTEMD_UNIT=ssh.service")
}
if config.Login.Local.Enabled {
units = append(units, "SYSLOG_IDENTIFIER=login")
}
if config.Login.Su.Enabled {
units = append(units, "SYSLOG_IDENTIFIER=su")
}
if config.Login.Sudo.Enabled {
units = append(units, "SYSLOG_IDENTIFIER=sudo")
}
}
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
analysisService := analyzerLog.NewAnalysis(&config, logger, notify)
return &analyzer{
config: config,
logger: logger,
notify: notify,
systemd: systemdService,
analysis: analysisService,
logChan: make(chan analysisServices.Entry, 1000),
}
}
func (a *analyzer) Run(ctx context.Context) {
go a.systemd.Run(ctx, a.logChan)
go a.processLogs(ctx)
a.logger.Debug("Analyzer is start")
}
func (a *analyzer) processLogs(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case entry, ok := <-a.logChan:
if !ok {
// Channel closed
return
}
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
switch {
case entry.Unit == "ssh.service":
if err := a.analysis.SSH(&entry); err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
}
case entry.SyslogIdentifier == "login":
if err := a.analysis.Locale(&entry); err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze locale logs: %s", err))
}
case entry.SyslogIdentifier == "sudo":
if err := a.analysis.Sudo(&entry); err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze sudo logs: %s", err))
}
case entry.SyslogIdentifier == "su":
if err := a.analysis.Su(&entry); err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze su logs: %s", err))
}
default:
a.logger.Debug(fmt.Sprintf("Unknown unit or SyslogIdentifier: %s", entry.Unit))
}
}
}
}
func (a *analyzer) Close() error {
if err := a.systemd.Close(); err != nil {
return err
}
close(a.logChan)
a.logger.Debug("Analyzer is stop")
return nil
}

View File

@@ -0,0 +1,5 @@
package config
type BinPath struct {
Journalctl string
}

View File

@@ -0,0 +1,6 @@
package config
type Config struct {
BinPath BinPath
Login Login
}

View File

@@ -0,0 +1,30 @@
package config
type Login struct {
Enabled bool
Notify bool
SSH LoginSSH
Local LoginLocal
Su LoginSu
Sudo LoginSudo
}
type LoginSSH struct {
Enabled bool
Notify bool
}
type LoginLocal struct {
Enabled bool
Notify bool
}
type LoginSu struct {
Enabled bool
Notify bool
}
type LoginSudo struct {
Enabled bool
Notify bool
}

View File

@@ -0,0 +1,52 @@
package log
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Analysis interface {
SSH(entry *analysisServices.Entry) error
Locale(entry *analysisServices.Entry) error
Su(entry *analysisServices.Entry) error
Sudo(entry *analysisServices.Entry) error
}
type analysis struct {
sshService analysisServices.Analysis
localeService analysisServices.Analysis
suService analysisServices.Analysis
sudoService analysisServices.Analysis
logger log.Logger
notify notifications.Notifications
}
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
return &analysis{
sshService: analysisServices.NewSSH(config, logger, notify),
localeService: analysisServices.NewLocale(config, logger, notify),
suService: analysisServices.NewSu(config, logger, notify),
sudoService: analysisServices.NewSudo(config, logger, notify),
logger: logger,
notify: notify,
}
}
func (a *analysis) SSH(entry *analysisServices.Entry) error {
return a.sshService.Process(entry)
}
func (a *analysis) Locale(entry *analysisServices.Entry) error {
return a.localeService.Process(entry)
}
func (a *analysis) Su(entry *analysisServices.Entry) error {
return a.suService.Process(entry)
}
func (a *analysis) Sudo(entry *analysisServices.Entry) error {
return a.sudoService.Process(entry)
}

View File

@@ -0,0 +1,29 @@
package analysis
import (
"time"
)
type Analysis interface {
Process(entry *Entry) error
}
type Entry struct {
Message string
Unit string
PID string
SyslogIdentifier string
Time time.Time
}
type processReturn struct {
found bool
subject string
body string
}
type EmptyAnalysis struct{}
func (empty *EmptyAnalysis) Process(_ *Entry) error {
return nil
}

View File

@@ -0,0 +1,78 @@
package analysis
import (
"fmt"
"regexp"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type locale struct {
login localeLogin
logger log.Logger
notify notifications.Notifications
}
type localeLogin struct {
enabled bool
notify bool
}
func NewLocale(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
if !config.Login.Enabled || !config.Login.Local.Enabled {
return &EmptyAnalysis{}
}
return &locale{
login: localeLogin{
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
notify: config.Login.Notify && config.Login.SSH.Notify,
},
logger: logger,
notify: notify,
}
}
func (l *locale) Process(entry *Entry) error {
if l.login.enabled {
result, err := l.login.process(entry)
if err != nil {
l.logger.Error(fmt.Sprintf("Failed to process TTY login: %s", err))
} else if result.found {
if l.login.notify {
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
}
l.logger.Info(fmt.Sprintf("TTY login detected: %s", entry.Message))
}
}
return nil
}
func (l *localeLogin) process(entry *Entry) (processReturn, error) {
re := regexp.MustCompile(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`)
matches := re.FindStringSubmatch(entry.Message)
if matches != nil {
user := matches[1]
return processReturn{
found: true,
subject: i18n.Lang.T("alert.login.locale.subject", map[string]any{
"User": user,
}),
body: i18n.Lang.T("alert.login.locale.body", map[string]any{
"User": user,
"Log": entry.Message,
"Time": entry.Time,
}),
}, nil
}
return processReturn{found: false}, nil
}

View File

@@ -0,0 +1,81 @@
package analysis
import (
"fmt"
"regexp"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type ssh struct {
login sshLogin
logger log.Logger
notify notifications.Notifications
}
type sshLogin struct {
enabled bool
notify bool
}
func NewSSH(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
if !config.Login.Enabled || !config.Login.SSH.Enabled {
return &EmptyAnalysis{}
}
return &ssh{
login: sshLogin{
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
notify: config.Login.Notify && config.Login.SSH.Notify,
},
logger: logger,
notify: notify,
}
}
func (s *ssh) Process(entry *Entry) error {
if s.login.enabled {
result, err := s.login.process(entry)
if err != nil {
s.logger.Error(fmt.Sprintf("Failed to process ssh login: %s", err))
} else if result.found {
if s.login.notify {
s.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
}
s.logger.Info(fmt.Sprintf("SSH login detected: %s", entry.Message))
}
}
return nil
}
func (l *sshLogin) process(entry *Entry) (processReturn, error) {
re := regexp.MustCompile(`^Accepted (\S+) for (\S+) from (\S+) port \S+`)
matches := re.FindStringSubmatch(entry.Message)
if matches != nil {
user := matches[2]
ip := matches[3]
return processReturn{
found: true,
subject: i18n.Lang.T("alert.login.ssh.subject", map[string]any{
"User": user,
"IP": ip,
}),
body: i18n.Lang.T("alert.login.ssh.body", map[string]any{
"User": user,
"IP": ip,
"Log": entry.Message,
"Time": entry.Time,
}),
}, nil
}
return processReturn{found: false}, nil
}

View File

@@ -0,0 +1,81 @@
package analysis
import (
"fmt"
"regexp"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type su struct {
login suLogin
logger log.Logger
notify notifications.Notifications
}
type suLogin struct {
enabled bool
notify bool
}
func NewSu(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
if !config.Login.Enabled || !config.Login.Su.Enabled {
return &EmptyAnalysis{}
}
return &su{
login: suLogin{
enabled: config.Login.Enabled && config.Login.Su.Enabled,
notify: config.Login.Notify && config.Login.Su.Notify,
},
logger: logger,
notify: notify,
}
}
func (l *su) Process(entry *Entry) error {
if l.login.enabled {
result, err := l.login.process(entry)
if err != nil {
l.logger.Error(fmt.Sprintf("Failed to process Su login: %s", err))
} else if result.found {
if l.login.notify {
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
}
l.logger.Info(fmt.Sprintf("Su login detected: %s", entry.Message))
}
}
return nil
}
func (l *suLogin) process(entry *Entry) (processReturn, error) {
re := regexp.MustCompile(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
matches := re.FindStringSubmatch(entry.Message)
if matches != nil {
user := matches[1]
byUser := matches[2]
return processReturn{
found: true,
subject: i18n.Lang.T("alert.login.su.subject", map[string]any{
"User": user,
"ByUser": byUser,
}),
body: i18n.Lang.T("alert.login.su.body", map[string]any{
"User": user,
"ByUser": byUser,
"Log": entry.Message,
"Time": entry.Time,
}),
}, nil
}
return processReturn{found: false}, nil
}

View File

@@ -0,0 +1,81 @@
package analysis
import (
"fmt"
"regexp"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type sudo struct {
login sudoLogin
logger log.Logger
notify notifications.Notifications
}
type sudoLogin struct {
enabled bool
notify bool
}
func NewSudo(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
if !config.Login.Enabled || !config.Login.Su.Enabled {
return &EmptyAnalysis{}
}
return &sudo{
login: sudoLogin{
enabled: config.Login.Enabled && config.Login.Sudo.Enabled,
notify: config.Login.Notify && config.Login.Sudo.Notify,
},
logger: logger,
notify: notify,
}
}
func (s *sudo) Process(entry *Entry) error {
if s.login.enabled {
result, err := s.login.process(entry)
if err != nil {
s.logger.Error(fmt.Sprintf("Failed to process Sudo login: %s", err))
} else if result.found {
if s.login.notify {
s.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
}
s.logger.Info(fmt.Sprintf("Sudo login detected: %s", entry.Message))
}
}
return nil
}
func (s *sudoLogin) process(entry *Entry) (processReturn, error) {
re := regexp.MustCompile(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
matches := re.FindStringSubmatch(entry.Message)
if matches != nil {
user := matches[1]
byUser := matches[2]
return processReturn{
found: true,
subject: i18n.Lang.T("alert.login.sudo.subject", map[string]any{
"User": user,
"ByUser": byUser,
}),
body: i18n.Lang.T("alert.login.sudo.body", map[string]any{
"User": user,
"ByUser": byUser,
"Log": entry.Message,
"Time": entry.Time,
}),
}, nil
}
return processReturn{found: false}, nil
}

View File

@@ -0,0 +1,151 @@
package log
import (
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"strconv"
"sync"
"time"
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Systemd interface {
Run(ctx context.Context, logChan chan<- analysisServices.Entry)
Close() error
}
type systemd struct {
path string
units []string
logger log.Logger
cmd *exec.Cmd
mu sync.Mutex
}
type journalRawEntry struct {
Message string `json:"MESSAGE"`
Unit string `json:"_SYSTEMD_UNIT"`
PID string `json:"_PID"`
SyslogIdentifier string `json:"SYSLOG_IDENTIFIER"`
SourceTimestamp string `json:"_SOURCE_REALTIME_TIMESTAMP"`
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
}
func NewSystemd(path string, units []string, logger log.Logger) Systemd {
return &systemd{
path: path,
units: units,
logger: logger,
}
}
func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
if len(s.units) == 0 {
s.logger.Debug("No units specified for journalctl")
return
}
s.logger.Debug("Journalctl started")
for {
select {
case <-ctx.Done():
return
default:
if err := s.watch(ctx, logChan); err != nil {
s.logger.Error(fmt.Sprintf("Journalctl exited with error: %v", err))
}
// Pause before restarting to avoid CPU load during persistent errors
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
s.logger.Warn("Journalctl connection lost. Restarting in 5s...")
continue
}
}
}
}
func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Entry) error {
args := []string{"-f", "-n", "0", "-o", "json"}
for index, unit := range s.units {
if index > 0 {
args = append(args, "+")
}
args = append(args, unit)
}
cmd := exec.CommandContext(ctx, s.path, args...)
s.mu.Lock()
s.cmd = cmd
s.mu.Unlock()
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe error: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start error: %w", err)
}
decoder := json.NewDecoder(stdout)
for {
var raw journalRawEntry
if err := decoder.Decode(&raw); err != nil {
if err == io.EOF {
break // The process terminated normally or was killed.
}
return fmt.Errorf("decode error: %w", err)
}
tsStr := raw.SourceTimestamp
if tsStr == "" {
tsStr = raw.RealtimeTimestamp
}
var entryTime time.Time
if usec, err := strconv.ParseInt(tsStr, 10, 64); err == nil {
entryTime = time.Unix(0, usec*int64(time.Microsecond))
} else {
entryTime = time.Now()
}
logChan <- analysisServices.Entry{
Message: raw.Message,
Unit: raw.Unit,
PID: raw.PID,
SyslogIdentifier: raw.SyslogIdentifier,
Time: entryTime,
}
}
return cmd.Wait()
}
func (s *systemd) Close() error {
if s.units == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.cmd != nil && s.cmd.Process != nil {
s.logger.Debug("Stopping journalctl")
// Force journalctl to quit on shutdown
return s.cmd.Process.Kill()
}
s.logger.Debug("Journalctl stopped")
return nil
}

View File

@@ -5,7 +5,10 @@ import (
"errors"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
@@ -17,10 +20,13 @@ type Daemon interface {
}
type daemon struct {
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
notifications notifications.Notifications
analyzer analyzer.Analyzer
docker docker_monitor.Docker
stopCh chan struct{}
}
@@ -52,6 +58,23 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
_ = d.socket.Close()
}()
d.notifications.Run()
defer func() {
_ = d.notifications.Close()
}()
d.analyzer.Run(ctx)
defer func() {
_ = d.analyzer.Close()
}()
if d.firewall.DockerSupport() {
go d.docker.Run()
defer func() {
_ = d.docker.Close()
}()
}
go d.socket.Run(ctx, d.socketCommand)
d.runWorker(ctx, isTesting, testingInterval)

View File

@@ -0,0 +1,87 @@
package chain
import nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
type Chains interface {
ForwardFilterJump(addRule func(expr ...string) error) error
PreroutingFilterJump(addRule func(expr ...string) error) error
PreroutingNatJump(addRule func(expr ...string) error) error
OutputNatJump(addRule func(expr ...string) error) error
PostroutingNatJump(addRule func(expr ...string) error) error
List() *chains
}
type chains struct {
ForwardFilter Data
ForwardBridge Data
ForwardCT Data
PreroutingFilter Data
DockerFilter Data
DockerFilterFirst Data
DockerFilterSecond Data
DockerNat Data
PostroutingNat Data
}
type Data struct {
chain nftChain.Chain
name string
}
func (d *chains) ForwardFilterJump(addRule func(expr ...string) error) error {
return d.ForwardFilter.Jump(addRule, "")
}
func (d *chains) PreroutingFilterJump(addRule func(expr ...string) error) error {
return d.PreroutingFilter.Jump(addRule, "")
}
func (d *chains) PreroutingNatJump(addRule func(expr ...string) error) error {
return d.DockerNat.Jump(addRule, "fib daddr type local counter")
}
func (d *chains) OutputNatJump(addRule func(expr ...string) error) error {
if err := d.DockerNat.Jump(addRule, "ip daddr != 127.0.0.0/8 fib daddr type local counter"); err != nil {
return err
}
return d.DockerNat.Jump(addRule, "ip6 daddr != ::1 fib daddr type local counter")
}
func (d *chains) PostroutingNatJump(addRule func(expr ...string) error) error {
return d.PostroutingNat.Jump(addRule, "")
}
func (d *chains) List() *chains {
return d
}
func (d *Data) Jump(addRule func(expr ...string) error, rule string) error {
args := []string{rule, "jump", d.name}
return addRule(args...)
}
func (d *Data) JumpTo(data *Data, rule string, comment string) error {
args := []string{rule, "jump", d.name, comment}
return data.AddRule(args...)
}
func (d *Data) AddRule(rule ...string) error {
return d.chain.AddRule(rule...)
}
func (d *Data) RemoveRuleByHandle(handle uint64) error {
return d.chain.RemoveRuleByHandle(handle)
}
func (d *Data) ListRules() ([]nftChain.Rule, error) {
return d.chain.ListRules()
}
func (d *Data) Clear() error {
return d.chain.Clear()
}

View File

@@ -0,0 +1,32 @@
package chain
type emptyChains struct {
}
func NewEmptyChains() Chains {
return &emptyChains{}
}
func (c *emptyChains) ForwardFilterJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PreroutingFilterJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PreroutingNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) OutputNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) PostroutingNatJump(_ func(expr ...string) error) error {
return nil
}
func (c *emptyChains) List() *chains {
return &chains{}
}

View File

@@ -0,0 +1,77 @@
package chain
import nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
func NewChains(newNoneChain func(chain string) (nftChain.Chain, error)) (Chains, error) {
chainsData := &chains{}
if data, err := newChainData("docker_nat", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerNat = data
}
if data, err := newChainData("docker_postrouting_nat", newNoneChain); err != nil {
return nil, err
} else {
chainsData.PostroutingNat = data
}
if data, err := newChainData("docker_prerouting_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.PreroutingFilter = data
}
if data, err := newChainData("docker_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilter = data
}
if data, err := newChainData("docker_filter_first", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilterFirst = data
}
if data, err := newChainData("docker_filter_second", newNoneChain); err != nil {
return nil, err
} else {
chainsData.DockerFilterSecond = data
}
if data, err := newChainData("docker_forward_filter", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardFilter = data
}
if data, err := newChainData("docker_forward_bridge", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardBridge = data
}
if data, err := newChainData("docker_forward_ct", newNoneChain); err != nil {
return nil, err
} else {
chainsData.ForwardCT = data
}
return chainsData, nil
}
func newChainData(chainName string, newNoneChain func(chain string) (nftChain.Chain, error)) (Data, error) {
data := Data{
name: chainName,
}
newChain, err := newNoneChain(data.name)
if err != nil {
return data, err
}
data.chain = newChain
return data, nil
}

View File

@@ -0,0 +1,42 @@
package client
import (
"encoding/json"
"fmt"
"strings"
)
func (d *docker) bridges() ([]string, error) {
args := []string{"network", "ls", "-q", "--filter", "Driver=bridge"}
result, err := d.command(args...)
if err != nil {
return nil, fmt.Errorf("failed to get docker bridge names: %s", err.Error())
}
output := strings.TrimSpace(string(result))
if output == "" {
return []string{}, nil
}
lines := strings.Split(output, "\n")
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
return lines, nil
}
func (d *docker) bridgeInfo(bridgeID string) (DockerBridgeInspect, error) {
args := []string{"network", "inspect", bridgeID}
result, err := d.command(args...)
if err != nil {
return DockerBridgeInspect{}, fmt.Errorf("failed to get bridge name: %s", err.Error())
}
var info []DockerBridgeInspect
if err := json.Unmarshal(result, &info); err != nil {
return DockerBridgeInspect{}, err
}
return info[0], nil
}

View File

@@ -0,0 +1,84 @@
package client
import (
"encoding/json"
"fmt"
"strings"
)
func (d *docker) containers(bridgeID string) ([]string, error) {
args := []string{"ps", "-q", "--no-trunc", "--filter", fmt.Sprintf("network=%s", bridgeID)}
result, err := d.command(args...)
if err != nil {
return nil, fmt.Errorf("failed to get docker containers: %s", err.Error())
}
output := strings.TrimSpace(string(result))
if output == "" {
return []string{}, nil
}
lines := strings.Split(output, "\n")
for i := range lines {
lines[i] = strings.TrimSpace(lines[i])
}
return lines, nil
}
func (d *docker) containerNetworks(containerID string) (DockerContainerInspect, error) {
result, err := d.command("inspect", containerID)
if err != nil {
return DockerContainerInspect{}, err
}
var info []DockerContainerInspect
if err := json.Unmarshal(result, &info); err != nil {
return DockerContainerInspect{}, err
}
if len(info) == 0 {
return DockerContainerInspect{}, fmt.Errorf("container %s not found", containerID)
}
return info[0], nil
}
func (d *docker) parsePorts(info DockerContainerInspect) []ContainerPort {
var ports []ContainerPort
for containerPortFull, hostConfigs := range info.NetworkSettings.Ports {
parts := strings.Split(containerPortFull, "/")
portNum := parts[0]
protocol := "tcp" // default
if len(parts) > 1 {
protocol = parts[1]
}
cp := ContainerPort{
Port: portNum,
Protocol: protocol,
}
for _, h := range hostConfigs {
host := HostPort{
Port: h.HostPort,
}
ipVersion, err := ipVersion(h.HostIp)
if err != nil {
d.logger.Error(err.Error())
continue
}
host.IP = IPInfo{Address: h.HostIp, Version: ipVersion}
cp.HostPort = append(cp.HostPort, host)
}
ports = append(ports, cp)
}
return ports
}

View File

@@ -0,0 +1,262 @@
package client
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os/exec"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Docker interface {
FetchBridges() (Bridges, error)
FetchBridge(bridgeID string) (Bridge, error)
FetchContainers(bridgeID string) (Containers, error)
FetchContainer(containerID string) (Container, error)
Events() <-chan Event
EventsClose() error
}
type docker struct {
path string
ctx context.Context
logger log.Logger
cmd *exec.Cmd
mu sync.Mutex
}
func NewDocker(path string, ctx context.Context, logger log.Logger) Docker {
return &docker{
path: path,
ctx: ctx,
logger: logger,
}
}
func (d *docker) FetchBridges() (Bridges, error) {
bridges := Bridges{}
list, err := d.bridges()
if err != nil {
return nil, err
}
for _, bridgeId := range list {
bridge, err := d.FetchBridge(bridgeId)
if err != nil {
d.logger.Error(err.Error())
continue
}
bridges = append(bridges, bridge)
}
return bridges, nil
}
func (d *docker) FetchBridge(bridgeID string) (Bridge, error) {
bridgeInfo, err := d.bridgeInfo(bridgeID)
if err != nil {
return Bridge{}, err
}
var containers Containers
containers, err = d.FetchContainers(bridgeID)
if err != nil {
d.logger.Error(err.Error())
}
bridgeName := bridgeInfo.Options.Name
if bridgeName == "" {
bridgeName = bridgeNameFromID(bridgeID)
}
var bridgeSubnet []string
if bridgeInfo.IPAM.Config != nil {
for _, config := range bridgeInfo.IPAM.Config {
bridgeSubnet = append(bridgeSubnet, config.Subnet)
}
}
return Bridge{
ID: bridgeInfo.ID,
Name: bridgeName,
Subnets: bridgeSubnet,
Containers: containers,
}, nil
}
func (d *docker) FetchContainers(bridgeID string) (Containers, error) {
containers := Containers{}
list, err := d.containers(bridgeID)
if err != nil {
return nil, err
}
for _, containerID := range list {
container, err := d.FetchContainer(containerID)
if err != nil {
d.logger.Error(err.Error())
continue
}
containers = append(containers, container)
}
return containers, nil
}
func (d *docker) FetchContainer(containerID string) (Container, error) {
info, err := d.containerNetworks(containerID)
if err != nil {
return Container{}, err
}
networks := ContainerNetworks{
IPAddresses: []IPInfo{},
Ports: d.parsePorts(info),
}
for _, networkData := range info.NetworkSettings.Networks {
if networkData.IPAddress != "" {
ipVesion, err := ipVersion(networkData.IPAddress)
if err != nil {
d.logger.Error(err.Error())
continue
}
networks.IPAddresses = append(networks.IPAddresses, IPInfo{
Address: networkData.IPAddress,
Version: ipVesion,
NetworkID: networkData.NetworkID,
})
}
}
return Container{
ID: containerID,
Networks: networks,
}, nil
}
func (d *docker) command(args ...string) ([]byte, error) {
cmd := exec.CommandContext(d.ctx, d.path, args...)
result, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf(string(result))
}
return result, nil
}
func (d *docker) Events() <-chan Event {
eventsChan := make(chan Event)
d.logger.Debug("Starting docker monitor")
go func() {
defer close(eventsChan)
for {
select {
case <-d.ctx.Done():
return
default:
if err := d.watch(eventsChan); err != nil {
d.logger.Error(fmt.Sprintf("Docker monitor exited with error: %v", err))
}
// Pause before restarting to avoid CPU load during persistent errors
select {
case <-d.ctx.Done():
return
case <-time.After(15 * time.Second):
d.logger.Warn("Docker connection lost. Restarting in 15s...")
continue
}
}
}
}()
return eventsChan
}
func (d *docker) watch(eventsChan chan Event) error {
args := []string{
"events",
"--filter", "type=container",
"--filter", "event=start",
"--filter", "event=die",
"--filter", "type=network",
"--filter", "event=create",
"--filter", "event=destroy",
"--format",
"{{json .}}",
}
cmd := exec.CommandContext(d.ctx, d.path, args...)
d.mu.Lock()
d.cmd = cmd
d.mu.Unlock()
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
if err := scanner.Err(); err != nil {
return err
}
if scanner.Text() == "" {
return fmt.Errorf("empty line")
}
var dockerEvent DockerEvent
if err := json.Unmarshal([]byte(scanner.Text()), &dockerEvent); err != nil {
return fmt.Errorf("failed to unmarshal docker event: %v", err)
}
if dockerEvent.Type == "" || dockerEvent.Action == "" || dockerEvent.Actor.ID == "" {
continue
}
eventsChan <- Event{
Type: dockerEvent.Type,
Action: dockerEvent.Action,
ID: dockerEvent.Actor.ID,
Message: scanner.Text(),
}
}
return scanner.Err()
}
func (d *docker) EventsClose() error {
d.mu.Lock()
defer d.mu.Unlock()
if d.cmd != nil && d.cmd.Process != nil {
d.logger.Debug("Stopping docker monitor")
// Force docker monitor to quit on shutdown
return d.cmd.Process.Kill()
}
d.logger.Debug("Docker monitor stopped")
return nil
}
func bridgeNameFromID(bridgeID string) string {
if len(bridgeID) > 12 {
bridgeID = bridgeID[:12]
}
return fmt.Sprintf("br-%s", bridgeID)
}

View File

@@ -0,0 +1,104 @@
package client
import (
"errors"
"net"
)
type Event struct {
Type string
Action string
ID string // Full 64-char ID (Actor.ID)
Message string // debug
}
type DockerEvent struct {
Type string `json:"Type"` // container, network
Action string `json:"Action"` // start, die, create, destroy
Actor struct {
ID string `json:"ID"`
} `json:"Actor"`
}
type Bridges []Bridge
type Bridge struct {
ID string
Name string
Subnets []string
Containers Containers
}
type DockerBridgeInspect struct {
ID string `json:"Id"`
Options struct {
Name string `json:"com.docker.network.bridge.name"`
} `json:"Options"`
IPAM struct {
Config []struct {
Subnet string `json:"Subnet"`
} `json:"Config"`
} `json:"IPAM"`
}
type Containers []Container
type Container struct {
ID string
Networks ContainerNetworks
}
type ContainerNetworks struct {
IPAddresses []IPInfo
Ports []ContainerPort
}
type IPInfo struct {
Address string
Version int // "4" or "6"
NetworkID string
}
func (i IPInfo) NftPrefix() string {
if i.Version == 6 {
return "ip6"
}
return "ip"
}
type ContainerPort struct {
Port string
Protocol string
HostPort []HostPort
}
type HostPort struct {
Port string
IP IPInfo
}
type DockerContainerInspect struct {
NetworkSettings struct {
Ports map[string][]struct {
HostIp string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"Ports"`
Networks map[string]struct {
IPAddress string `json:"IPAddress"`
NetworkID string `json:"NetworkID"`
} `json:"Networks"`
} `json:"NetworkSettings"`
}
func ipVersion(ip string) (int, error) {
ipParse := net.ParseIP(ip)
if ipParse == nil || (ipParse.To4() == nil && ipParse.To16() == nil) {
return 0, errors.New("invalid ip address")
}
if ipParse.To4() != nil {
return 4, nil
}
return 6, nil
}

View File

@@ -0,0 +1,13 @@
package docker_monitor
type Config struct {
Path string
RuleStrategy RuleStrategy
}
type RuleStrategy int8
const (
RuleStrategyRebuild RuleStrategy = iota + 1
RuleStrategyIncremental
)

View File

@@ -0,0 +1,74 @@
package docker_monitor
import (
"context"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/rule_strategy"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Docker interface {
NftReload(newNoneChain func(chain string) (nftChain.Chain, error)) error
NftChains() chain.Chains
Run()
Close() error
}
type docker struct {
dockerClient client.Docker
ruleStrategy rule_strategy.Strategy
logger log.Logger
ctx context.Context
}
func New(config *Config, ctx context.Context, logger log.Logger) (Docker, error) {
dockerClient := client.NewDocker(config.Path, ctx, logger)
ruleStrategy, err := newRuleStrategy(config, dockerClient, logger)
if err != nil {
return nil, err
}
return &docker{
dockerClient: dockerClient,
logger: logger,
ctx: ctx,
ruleStrategy: ruleStrategy,
}, nil
}
func (d *docker) NftReload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
return d.ruleStrategy.Reload(newNoneChain)
}
func (d *docker) NftChains() chain.Chains {
return d.ruleStrategy.Chains()
}
func (d *docker) Run() {
events := d.dockerClient.Events()
for {
select {
case <-d.ctx.Done():
return
case event := <-events:
if event.Message == "" {
continue
}
d.logger.Debug("Docker event received: " + event.Message)
d.ruleStrategy.Event(&event)
}
}
}
func (d *docker) Close() error {
return d.dockerClient.EventsClose()
}
func (d *docker) chainCommand(chainData chain.Data, rule string) {
if err := chainData.AddRule(rule); err != nil {
d.logger.Error(err.Error())
}
}

View File

@@ -0,0 +1,32 @@
package docker_monitor
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
)
type DockerNotSupport struct {
chains chain.Chains
}
func NewDockerNotSupport() Docker {
return &DockerNotSupport{
chains: chain.NewEmptyChains(),
}
}
func (d *DockerNotSupport) NftReload(_ func(chain string) (nftChain.Chain, error)) error {
return nil
}
func (d *DockerNotSupport) NftChains() chain.Chains {
return d.chains
}
func (d *DockerNotSupport) Run() {
}
func (d *DockerNotSupport) Close() error {
return nil
}

View File

@@ -0,0 +1,22 @@
package docker_monitor
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/rule_strategy"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
func newRuleStrategy(config *Config, dockerClient client.Docker, logger log.Logger) (rule_strategy.Strategy, error) {
generate := rule_strategy.NewGenerator(dockerClient, logger)
switch config.RuleStrategy {
case RuleStrategyRebuild:
return rule_strategy.NewRebuildStrategy(generate), nil
case RuleStrategyIncremental:
return rule_strategy.NewIncrementalStrategy(generate, dockerClient, logger), nil
}
return nil, fmt.Errorf("invalid option rule_strategy")
}

View File

@@ -0,0 +1,174 @@
package rule_strategy
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type Generator interface {
GenerateAll(chains chain.Chains, isComment bool)
GenerateBridge(bridge client.Bridge, chain chain.Chains, isComment bool)
GenerateContainer(container client.Container, bridgeName string, chain chain.Chains, isComment bool)
ClearChains(chains chain.Chains)
AddRule(chainData chain.Data, rule string)
}
type generator struct {
dockerClient client.Docker
logger log.Logger
}
func NewGenerator(dockerClient client.Docker, logger log.Logger) Generator {
return &generator{
dockerClient: dockerClient,
logger: logger,
}
}
func (g *generator) GenerateAll(chains chain.Chains, isComment bool) {
listChains := chains.List()
if err := listChains.ForwardCT.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.ForwardBridge.JumpTo(&listChains.ForwardFilter, "", ""); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.DockerFilterFirst.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.DockerFilterSecond.JumpTo(&listChains.DockerFilter, "", ""); err != nil {
g.logger.Error(err.Error())
}
bridges, err := g.dockerClient.FetchBridges()
if err != nil {
g.logger.Error(err.Error())
return
}
for _, bridge := range bridges {
g.GenerateBridge(bridge, chains, isComment)
if bridge.Containers == nil {
continue
}
for _, container := range bridge.Containers {
g.GenerateContainer(container, bridge.Name, chains, isComment)
}
}
}
func (g *generator) GenerateBridge(bridge client.Bridge, chain chain.Chains, isComment bool) {
listChains := chain.List()
var rule string
comment := ""
if isComment {
comment = fmt.Sprintf("comment \"bridge_id:%s\"", bridge.ID)
}
rule = fmt.Sprintf("iifname != \"%s\" oifname \"%s\" counter drop %s", bridge.Name, bridge.Name, comment)
g.AddRule(listChains.DockerFilterSecond, rule)
rule = fmt.Sprintf("iifname \"%s\" counter accept %s", bridge.Name, comment)
g.AddRule(listChains.ForwardFilter, rule)
rule = fmt.Sprintf("oifname \"%s\" counter", bridge.Name)
if err := listChains.DockerFilter.JumpTo(&listChains.ForwardBridge, rule, comment); err != nil {
g.logger.Error(err.Error())
}
rule = fmt.Sprintf("oifname \"%s\" ct state related,established counter accept %s", bridge.Name, comment)
g.AddRule(listChains.ForwardCT, rule)
for _, subnet := range bridge.Subnets {
rule = fmt.Sprintf("ip saddr %s oifname != \"%s\" counter masquerade %s", subnet, bridge.Name, comment)
g.AddRule(listChains.PostroutingNat, rule)
}
}
func (g *generator) GenerateContainer(container client.Container, bridgeName string, chain chain.Chains, isComment bool) {
listChains := chain.List()
var rule string
comment := ""
if isComment {
comment = fmt.Sprintf("comment \"container_id:%s\"", container.ID)
}
for _, ipInfo := range container.Networks.IPAddresses {
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" counter drop %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, comment)
g.AddRule(listChains.PreroutingFilter, rule)
for _, port := range container.Networks.Ports {
isZeroAddress := false
for _, hostInfo := range port.HostPort {
if hostInfo.IP.Address != "0.0.0.0" && hostInfo.IP.Address != "::" && (hostInfo.IP.Address == "127.0.0.1" || hostInfo.IP.Address == "::1") {
rule = fmt.Sprintf("%s daddr %s iifname != \"lo\" %s dport %s counter drop %s", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, port.Protocol, hostInfo.Port, comment)
g.AddRule(listChains.PreroutingFilter, rule)
}
if hostInfo.IP.Address == "0.0.0.0" || hostInfo.IP.Address == "::" {
if isZeroAddress {
continue
}
isZeroAddress = true
rule = fmt.Sprintf("iifname != \"%s\" %s dport %s counter dnat %s to %s:%s %s", bridgeName, port.Protocol, hostInfo.Port, ipInfo.NftPrefix(), ipInfo.Address, port.Port, comment)
g.AddRule(listChains.DockerNat, rule)
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, bridgeName, port.Protocol, port.Port, comment)
g.AddRule(listChains.DockerFilterFirst, rule)
continue
}
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" oifname \"%s\" %s dport %s counter accept %s", ipInfo.NftPrefix(), ipInfo.Address, bridgeName, bridgeName, port.Protocol, port.Port, comment)
g.AddRule(listChains.DockerFilterFirst, rule)
rule = fmt.Sprintf("%s daddr %s iifname != \"%s\" %s dport %s counter dnat to %s:%s %s", hostInfo.IP.NftPrefix(), hostInfo.IP.Address, bridgeName, port.Protocol, hostInfo.Port, ipInfo.Address, port.Port, comment)
g.AddRule(listChains.DockerNat, rule)
}
}
}
}
func (g *generator) ClearChains(chains chain.Chains) {
listChains := chains.List()
if err := listChains.DockerNat.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.PostroutingNat.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.PreroutingFilter.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.DockerFilter.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.DockerFilterFirst.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.DockerFilterSecond.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.ForwardFilter.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.ForwardBridge.Clear(); err != nil {
g.logger.Error(err.Error())
}
if err := listChains.ForwardCT.Clear(); err != nil {
g.logger.Error(err.Error())
}
}
func (g *generator) AddRule(chainData chain.Data, rule string) {
if err := chainData.AddRule(rule); err != nil {
g.logger.Error(err.Error())
}
}

View File

@@ -0,0 +1,13 @@
package rule_strategy
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
)
type Strategy interface {
Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error
Chains() chain.Chains
Event(event *client.Event)
}

View File

@@ -0,0 +1,181 @@
package rule_strategy
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type incrementalStrategy struct {
dockerClient client.Docker
chains chain.Chains
generator Generator
logger log.Logger
}
func NewIncrementalStrategy(generator Generator, dockerClient client.Docker, logger log.Logger) Strategy {
return &incrementalStrategy{
dockerClient: dockerClient,
generator: generator,
logger: logger,
}
}
func (i *incrementalStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
chains, err := chain.NewChains(newNoneChain)
if err != nil {
return err
}
i.chains = chains
i.generator.GenerateAll(i.chains, true)
return nil
}
func (i *incrementalStrategy) Chains() chain.Chains {
return i.chains
}
func (i *incrementalStrategy) Event(event *client.Event) {
if event == nil || event.ID == "" {
return
}
if event.Type == "container" {
if event.Action == "start" {
if err := i.eventContainerStart(event.ID); err != nil {
i.logger.Error(fmt.Sprintf("failed to handle container start event: %s", err))
}
return
}
if event.Action == "die" {
i.eventContainerStop(event.ID)
return
}
return
}
if event.Type == "network" {
if event.Action == "create" {
if err := i.eventNetworkCreate(event.ID); err != nil {
i.logger.Error(fmt.Sprintf("failed to handle network create event: %s", err))
}
}
if event.Action == "destroy" {
i.eventNetworkDestroy(event.ID)
}
return
}
}
func (i *incrementalStrategy) eventContainerStart(containerId string) error {
container, err := i.dockerClient.FetchContainer(containerId)
if err != nil {
return err
}
for _, ipInfo := range container.Networks.IPAddresses {
bridge, err := i.dockerClient.FetchBridge(ipInfo.NetworkID)
if err != nil {
i.logger.Error(fmt.Sprintf("failed to fetch bridge for container %s: %s", containerId, err))
continue
}
i.generator.GenerateContainer(container, bridge.Name, i.chains, true)
}
return nil
}
func (i *incrementalStrategy) eventContainerStop(containerId string) {
listChains := i.chains.List()
if err := i.nftRuleDeleteContainer(containerId, &listChains.PreroutingFilter); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
}
if err := i.nftRuleDeleteContainer(containerId, &listChains.DockerNat); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
}
if err := i.nftRuleDeleteContainer(containerId, &listChains.DockerFilterFirst); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete container %s rules: %s", containerId, err))
}
}
func (i *incrementalStrategy) nftRuleDeleteContainer(containerId string, chain *chain.Data) error {
rules, err := chain.ListRules()
if err != nil {
return err
}
for _, rule := range rules {
if rule.Comment != "container_id:"+containerId {
continue
}
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete container %s rule: %s", containerId, err))
}
}
return nil
}
func (i *incrementalStrategy) eventNetworkCreate(bridgeId string) error {
bridge, err := i.dockerClient.FetchBridge(bridgeId)
if err != nil {
return err
}
i.generator.GenerateBridge(bridge, i.chains, true)
return nil
}
func (i *incrementalStrategy) eventNetworkDestroy(bridgeId string) {
listChains := i.chains.List()
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.DockerFilterSecond); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
}
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardFilter); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
}
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardBridge); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
}
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.ForwardCT); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
}
if err := i.nftRuleDeleteBridge(bridgeId, &listChains.PostroutingNat); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rules: %s", bridgeId, err))
}
}
func (i *incrementalStrategy) nftRuleDeleteBridge(bridgeId string, chain *chain.Data) error {
rules, err := chain.ListRules()
if err != nil {
return err
}
for _, rule := range rules {
if rule.Comment != "bridge_id:"+bridgeId {
continue
}
if err := chain.RemoveRuleByHandle(rule.Handle); err != nil {
i.logger.Error(fmt.Sprintf("failed to delete bridge %s rule: %s", bridgeId, err))
}
}
return nil
}

View File

@@ -0,0 +1,43 @@
package rule_strategy
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/client"
nftChain "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
)
type rebuildStrategy struct {
chains chain.Chains
generator Generator
}
func NewRebuildStrategy(generator Generator) Strategy {
return &rebuildStrategy{
generator: generator,
}
}
func (r *rebuildStrategy) Reload(newNoneChain func(chain string) (nftChain.Chain, error)) error {
chains, err := chain.NewChains(newNoneChain)
if err != nil {
return err
}
r.chains = chains
r.generator.GenerateAll(r.chains, false)
return nil
}
func (r *rebuildStrategy) Chains() chain.Chains {
return r.chains
}
func (r *rebuildStrategy) Event(event *client.Event) {
if event == nil || event.Type != "container" {
return
}
r.generator.ClearChains(r.chains)
r.generator.GenerateAll(r.chains, false)
}

View File

@@ -0,0 +1,67 @@
package chain
import (
"encoding/json"
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
)
type Chain interface {
AddRule(expr ...string) error
ListRules() ([]Rule, error)
RemoveRuleByHandle(handle uint64) error
Clear() error
}
type chain struct {
nft nft.NFT
family family.Type
table string
chain string
}
type NftOutput struct {
Nftables []NftElement `json:"nftables"`
}
type NftElement struct {
Rule *Rule `json:"rule,omitempty"`
}
type Rule struct {
Handle uint64 `json:"handle"`
Comment string `json:"comment"`
}
func (c *chain) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
}
func (c *chain) ListRules() ([]Rule, error) {
args := []string{"-a", "-j", "list", "chain", c.family.String(), c.table, c.chain}
jsonData, err := c.nft.Command().RunWithOutput(args...)
if err != nil {
return nil, err
}
var output NftOutput
if err := json.Unmarshal([]byte(jsonData), &output); err != nil {
return nil, err
}
var rules []Rule
for _, el := range output.Nftables {
if el.Rule != nil {
rules = append(rules, *el.Rule)
}
}
return rules, nil
}
func (c *chain) RemoveRuleByHandle(handle uint64) error {
return c.nft.Rule().Delete(c.family, c.table, c.chain, handle)
}
func (c *chain) Clear() error {
return c.nft.Chain().Clear(c.family, c.table, c.chain)
}

View File

@@ -1,7 +1,10 @@
package chain
import (
"strings"
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
nftFamily "git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
)
@@ -9,13 +12,13 @@ type Chains interface {
NewPacketFilter(enable bool) error
PacketFilter() PacketFilter
NewInput(chain string, defaultAllow bool) error
NewInput(chain string, defaultAllow bool, priority int) error
Input() Input
NewOutput(chain string, defaultAllow bool) error
NewOutput(chain string, defaultAllow bool, priority int) error
Output() Output
NewForward(chain string, defaultAllow bool) error
NewForward(chain string, defaultAllow bool, priority int) error
Forward() Forward
NewLocalInput() error
@@ -23,6 +26,14 @@ type Chains interface {
NewLocalOutput() error
LocalOutput() LocalOutput
NewLocalForward() error
LocalForward() LocalForward
ClearRules() error
NewNoneChain(chain string) (Chain, error)
NewChain(chain string, baseChain nftChain.ChainOptions) (Chain, error)
}
type chains struct {
@@ -31,8 +42,9 @@ type chains struct {
forward Forward
packetFilter PacketFilter
localInput LocalInput
localOutput LocalOutput
localInput LocalInput
localOutput LocalOutput
localForward LocalForward
family nftFamily.Type
table string
@@ -40,11 +52,12 @@ type chains struct {
}
func NewChains(nft nft.NFT, table string) (Chains, error) {
if err := nft.Clear(); err != nil {
family := nftFamily.INET
if err := clearRules(nft, family, table); err != nil {
return nil, err
}
family := nftFamily.INET
if err := nft.Table().Add(family, table); err != nil {
return nil, err
}
@@ -70,8 +83,8 @@ func (c *chains) PacketFilter() PacketFilter {
return c.packetFilter
}
func (c *chains) NewInput(chain string, defaultAllow bool) error {
input, err := newInput(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewInput(chain string, defaultAllow bool, priority int) error {
input, err := newInput(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -84,8 +97,8 @@ func (c *chains) Input() Input {
return c.input
}
func (c *chains) NewOutput(chain string, defaultAllow bool) error {
output, err := newOutput(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewOutput(chain string, defaultAllow bool, priority int) error {
output, err := newOutput(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -98,8 +111,8 @@ func (c *chains) Output() Output {
return c.output
}
func (c *chains) NewForward(chain string, defaultAllow bool) error {
forward, err := newForward(c.nft, c.family, c.table, chain, defaultAllow)
func (c *chains) NewForward(chain string, defaultAllow bool, priority int) error {
forward, err := newForward(c.nft, c.family, c.table, chain, defaultAllow, priority)
if err != nil {
return err
}
@@ -137,3 +150,47 @@ func (c *chains) NewLocalOutput() error {
func (c *chains) LocalOutput() LocalOutput {
return c.localOutput
}
func (c *chains) NewLocalForward() error {
localForward, err := newLocalForward(c.nft, c.family, c.table)
if err != nil {
return err
}
c.localForward = localForward
return nil
}
func (c *chains) LocalForward() LocalForward {
return c.localForward
}
func (c *chains) ClearRules() error {
return clearRules(c.nft, c.family, c.table)
}
func (c *chains) NewNoneChain(chainName string) (Chain, error) {
return c.NewChain(chainName, nftChain.TypeNone)
}
func (c *chains) NewChain(chainName string, baseChain nftChain.ChainOptions) (Chain, error) {
if err := c.nft.Chain().Add(c.family, c.table, chainName, baseChain); err != nil {
return nil, err
}
return &chain{
nft: c.nft,
family: c.family,
table: c.table,
chain: chainName,
}, nil
}
func clearRules(nft nft.NFT, family nftFamily.Type, table string) error {
if err := nft.Table().Delete(family, table); err != nil {
if !strings.Contains(string(err.Error()), "delete table "+family.String()+" "+table) {
return err
}
}
return nil
}

View File

@@ -17,7 +17,7 @@ type forward struct {
chain string
}
func newForward(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Forward, error) {
func newForward(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Forward, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newForward(nft nft.NFT, family family.Type, table string, chain string, def
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookForward,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -17,7 +17,7 @@ type input struct {
chain string
}
func newInput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Input, error) {
func newInput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Input, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newInput(nft nft.NFT, family family.Type, table string, chain string, defau
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookInput,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -0,0 +1,41 @@
package chain
import (
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
)
type LocalForward interface {
AddRule(expr ...string) error
AddRuleIn(AddRuleFunc func(expr ...string) error) error
}
type localForward struct {
nft nft.NFT
family family.Type
table string
chain string
}
func newLocalForward(nft nft.NFT, family family.Type, table string) (LocalForward, error) {
chain := "local-forward"
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
return nil, err
}
return &localForward{
nft: nft,
family: family,
table: table,
chain: chain,
}, nil
}
func (l *localForward) AddRule(expr ...string) error {
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
}
func (l *localForward) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
}

View File

@@ -32,10 +32,10 @@ func newLocalInput(nft nft.NFT, family family.Type, table string) (LocalInput, e
}, nil
}
func (c *localInput) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
func (l *localInput) AddRule(expr ...string) error {
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
}
func (f *localInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + f.chain)
func (l *localInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
}

View File

@@ -32,10 +32,10 @@ func newLocalOutput(nft nft.NFT, family family.Type, table string) (LocalOutput,
}, nil
}
func (c *localOutput) AddRule(expr ...string) error {
return c.nft.Rule().Add(c.family, c.table, c.chain, expr...)
func (l *localOutput) AddRule(expr ...string) error {
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
}
func (f *localOutput) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("oifname != \"lo\" counter jump " + f.chain)
func (l *localOutput) AddRuleOut(AddRuleFunc func(expr ...string) error) error {
return AddRuleFunc("oifname != \"lo\" counter jump " + l.chain)
}

View File

@@ -17,7 +17,7 @@ type output struct {
chain string
}
func newOutput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool) (Output, error) {
func newOutput(nft nft.NFT, family family.Type, table string, chain string, defaultAllow bool, priority int) (Output, error) {
policy := nftChain.PolicyDrop
if defaultAllow {
policy = nftChain.PolicyAccept
@@ -26,7 +26,7 @@ func newOutput(nft nft.NFT, family family.Type, table string, chain string, defa
baseChain := nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookOutput,
Priority: 0,
Priority: int32(priority),
Policy: policy,
Device: "",
}

View File

@@ -1,6 +1,8 @@
package firewall
import "fmt"
import (
"fmt"
)
type Config struct {
InPorts []ConfigPort
@@ -13,11 +15,13 @@ type Config struct {
}
type ConfigOptions struct {
ClearMode ClearMode
SavesRules bool
SavesRulesPath string
DnsStrict bool
DnsStrictNs bool
PacketFilter bool
DockerSupport bool
}
type ConfigMetadata struct {
@@ -32,8 +36,11 @@ type ConfigPolicy struct {
DefaultAllowOutput bool
DefaultAllowForward bool
InputDrop PolicyDrop
InputPriority int
OutputDrop PolicyDrop
OutputPriority int
ForwardDrop PolicyDrop
ForwardPriority int
}
type PolicyDrop int8
@@ -143,3 +150,10 @@ func (d Direction) String() string {
return fmt.Sprintf("Direction(%d)", d)
}
}
type ClearMode int8
const (
ClearModeGlobal ClearMode = iota + 1
ClearModeOwn
)

View File

@@ -0,0 +1,72 @@
package firewall
import nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
func (f *firewall) reloadDocker() error {
f.logger.Debug("Reload docker rules")
if err := f.reloadDockerPrerouting(); err != nil {
return err
}
return nil
}
func (f *firewall) reloadDockerPrerouting() error {
preroutingNat, err := f.chains.NewChain("prerouting_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookPrerouting,
Priority: -100,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PreroutingNatJump(preroutingNat.AddRule); err != nil {
return err
}
preroutingFilter, err := f.chains.NewChain("prerouting_filter", nftChain.BaseChainOptions{
Type: nftChain.TypeFilter,
Hook: nftChain.HookPrerouting,
Priority: -300,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PreroutingFilterJump(preroutingFilter.AddRule); err != nil {
return err
}
outputNat, err := f.chains.NewChain("output_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookOutput,
Priority: -100,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().OutputNatJump(outputNat.AddRule); err != nil {
return err
}
postroutingNat, err := f.chains.NewChain("postrouting_nat", nftChain.BaseChainOptions{
Type: nftChain.TypeNat,
Hook: nftChain.HookPostrouting,
Priority: 300,
Policy: nftChain.PolicyAccept,
Device: "",
})
if err != nil {
return err
}
if err := f.docker.NftChains().PostroutingNatJump(postroutingNat.AddRule); err != nil {
return err
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
@@ -19,6 +20,8 @@ type API interface {
// ClearRules Clear all rules.
ClearRules()
DockerSupport() bool
}
type firewall struct {
@@ -26,9 +29,10 @@ type firewall struct {
logger log.Logger
config *Config
chains chain.Chains
docker docker_monitor.Docker
}
func New(pathNFT string, logger log.Logger, config Config) (API, error) {
func New(pathNFT string, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
nft, err := nftables.NewWithPath(pathNFT)
if err != nil {
return nil, fmt.Errorf("failed to create nft client: %w %s", err, pathNFT)
@@ -38,16 +42,28 @@ func New(pathNFT string, logger log.Logger, config Config) (API, error) {
nft: nft,
logger: logger,
config: &config,
docker: docker,
}, nil
}
func (f *firewall) Reload() error {
f.logger.Debug("Reload nftables rules")
if f.config.Options.ClearMode == ClearModeGlobal {
if err := f.nft.Clear(); err != nil {
return err
}
}
chains, err := chain.NewChains(f.nft, f.config.MetadataNaming.TableName)
if err != nil {
return err
}
f.chains = chains
if err := f.docker.NftReload(f.chains.NewNoneChain); err != nil {
return err
}
if err := f.chains.NewPacketFilter(f.config.Options.PacketFilter); err != nil {
return err
}
@@ -60,6 +76,11 @@ func (f *firewall) Reload() error {
if err := f.reloadForward(); err != nil {
return err
}
if f.config.Options.DockerSupport {
if err := f.reloadDocker(); err != nil {
return err
}
}
f.logger.Debug("Reload nftables rules done")
return nil
@@ -67,9 +88,20 @@ func (f *firewall) Reload() error {
func (f *firewall) ClearRules() {
f.logger.Debug("Clear nftables rules")
if err := f.nft.Clear(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
switch f.config.Options.ClearMode {
case ClearModeGlobal:
if err := f.nft.Clear(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
}
break
case ClearModeOwn:
if err := f.chains.ClearRules(); err != nil {
f.logger.Error(fmt.Sprintf("Failed to clear rules: %s", err))
}
break
}
f.logger.Debug("Clear nftables rules done")
}
@@ -100,3 +132,7 @@ func (f *firewall) SavesRules() {
f.logger.Info("Save nftables rules")
}
func (f *firewall) DockerSupport() bool {
return f.config.Options.DockerSupport
}

View File

@@ -2,12 +2,22 @@ package firewall
func (f *firewall) reloadForward() error {
f.logger.Debug("Reloading forward chain")
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward)
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward, f.config.Policy.ForwardPriority)
if err != nil {
return err
}
chain := f.chains.Forward()
if err := f.reloadForwardAddIPs(); err != nil {
return err
}
if f.config.Options.DockerSupport {
if err := f.docker.NftChains().ForwardFilterJump(chain.AddRule); err != nil {
return err
}
}
if f.config.Policy.DefaultAllowForward == false {
drop := f.config.Policy.ForwardDrop.String()
if err := chain.AddRule(drop); err != nil {
@@ -17,3 +27,53 @@ func (f *firewall) reloadForward() error {
return nil
}
func (f *firewall) reloadForwardAddIPs() error {
if err := f.chains.NewLocalForward(); err != nil {
return err
}
chain := f.chains.LocalForward()
if err := chain.AddRuleIn(f.chains.Forward().AddRule); err != nil {
return err
}
for _, ipConfig := range f.config.IP4.InIPs {
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
continue
}
if err := forwardAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
return err
}
}
if !f.config.IP6.Enable {
return nil
}
for _, ipConfig := range f.config.IP6.InIPs {
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
continue
}
if err := forwardAddIP(chain.AddRule, ipConfig, "ip6"); err != nil {
return err
}
}
return nil
}
func forwardAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch string) error {
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
// There, during routing, the port changes and then the IP blocking rule will not work.
//if !config.OnlyIP {
// rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
//}
rule += " counter " + config.Action.String()
if err := addRuleFunc(rule); err != nil {
return err
}
return nil
}

View File

@@ -10,7 +10,7 @@ import (
func (f *firewall) reloadInput() error {
f.logger.Debug("Reloading input chain")
err := f.chains.NewInput(f.config.MetadataNaming.ChainInputName, f.config.Policy.DefaultAllowInput)
err := f.chains.NewInput(f.config.MetadataNaming.ChainInputName, f.config.Policy.DefaultAllowInput, f.config.Policy.InputPriority)
if err != nil {
return err
}

View File

@@ -10,7 +10,7 @@ import (
func (f *firewall) reloadOutput() error {
f.logger.Debug("Reloading output chain")
err := f.chains.NewOutput(f.config.MetadataNaming.ChainOutputName, f.config.Policy.DefaultAllowOutput)
err := f.chains.NewOutput(f.config.MetadataNaming.ChainOutputName, f.config.Policy.DefaultAllowOutput, f.config.Policy.OutputPriority)
if err != nil {
return err
}

View File

@@ -0,0 +1,43 @@
package notifications
import (
"github.com/wneessen/go-mail"
)
type Config struct {
Enabled bool
ServerName string
Email Email
}
type Email struct {
Host string
Port uint
Username string
Password string
AuthType mail.SMTPAuthType
TLS TLS
From string
To string
}
type TLS struct {
Mode TLSMode
Policy TLSPolicy
Verify bool
}
type TLSMode string
const (
TLSModeNone TLSMode = "NONE"
TLSModeStartTLS TLSMode = "STARTTLS"
TLSModeImplicit TLSMode = "IMPLICIT"
)
type TLSPolicy string
const (
TLSPolicyMandatory TLSPolicy = "MANDATORY"
TLSPolicyOpportunistic TLSPolicy = "OPPORTUNISTIC"
)

View File

@@ -0,0 +1,151 @@
package notifications
import (
"context"
"crypto/tls"
"fmt"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"github.com/wneessen/go-mail"
)
type Message struct {
Subject string
Body string
}
type Notifications interface {
Run()
SendAsync(message Message)
Close() error
}
type notifications struct {
config Config
logger log.Logger
msgQueue chan Message
wg sync.WaitGroup
}
func New(config Config, logger log.Logger) Notifications {
return &notifications{
config: config,
logger: logger,
msgQueue: make(chan Message, 100),
}
}
func (n *notifications) Run() {
if n.config.Enabled == false {
n.logger.Info("Notifications are disabled")
}
n.wg.Add(1)
go func() {
defer n.wg.Done()
for msg := range n.msgQueue {
err := n.sendEmail(msg)
if err != nil {
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
} else if n.config.Enabled {
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
}
}
}()
}
func (n *notifications) SendAsync(message Message) {
select {
case n.msgQueue <- message:
if n.config.Enabled == false {
n.logger.Debug(fmt.Sprintf("email sending is disabled, message was added to the queue: Subject %s, Body %s", message.Subject, message.Body))
} else {
n.logger.Debug(fmt.Sprintf("added to the mail sending queue: Subject %s, Body %s", message.Subject, message.Body))
}
default:
n.logger.Error(fmt.Sprintf("failed to send email: queue is full"))
}
}
func (n *notifications) Close() error {
close(n.msgQueue)
n.logger.Debug("We are waiting for all notifications to be sent")
n.wg.Wait()
n.logger.Debug("Notifications queue processed and closed")
return nil
}
func (n *notifications) sendEmail(message Message) error {
if n.config.Enabled == false {
return nil
}
m := mail.NewMsg()
if err := m.From(n.config.Email.From); err != nil {
return err
}
if err := m.To(n.config.Email.To); err != nil {
return err
}
m.Subject(message.Subject + " (" + n.config.ServerName + ")")
m.SetBodyString(mail.TypeTextPlain, "Server: "+n.config.ServerName+"\n"+message.Body)
client, err := newClient(n.config.Email)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return client.DialAndSendWithContext(ctx, m)
}
func newClient(config Email) (*mail.Client, error) {
options := []mail.Option{
mail.WithPort(int(config.Port)),
mail.WithSMTPAuth(config.AuthType),
}
if config.AuthType != mail.SMTPAuthNoAuth {
options = append(options, mail.WithUsername(config.Username), mail.WithPassword(config.Password))
}
switch config.TLS.Mode {
case TLSModeImplicit:
options = append(options, mail.WithSSL())
break
case TLSModeStartTLS:
switch config.TLS.Policy {
case TLSPolicyMandatory:
options = append(options, mail.WithTLSPolicy(mail.TLSMandatory))
break
case TLSPolicyOpportunistic:
options = append(options, mail.WithTLSPolicy(mail.TLSOpportunistic))
break
default:
return nil, fmt.Errorf("unknown tls policy: %s", config.TLS.Policy)
}
if !config.TLS.Verify {
tlsCfg := &tls.Config{
InsecureSkipVerify: true,
}
options = append(options, mail.WithTLSConfig(tlsCfg))
}
break
case TLSModeNone:
break
default:
return nil, fmt.Errorf("unknown tls mode: %s", config.TLS.Mode)
}
options = append(options, mail.WithSSL())
return mail.NewClient(config.Host, options...)
}

View File

@@ -1,10 +1,14 @@
package daemon
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
)
type DaemonOptions struct {
PathPidFile string
PathSocketFile string
PathNftables string
ConfigFirewall firewall.Config
ConfigAnalyzer config.Config
}

View File

@@ -3,13 +3,16 @@ package daemon
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
firewall2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
func NewDaemon(opts DaemonOptions, logger log.Logger) (Daemon, error) {
func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notifications.Notifications, docker docker_monitor.Docker) (Daemon, error) {
if logger == nil {
return nil, errors.New("logger is nil")
}
@@ -24,12 +27,17 @@ func NewDaemon(opts DaemonOptions, logger log.Logger) (Daemon, error) {
return nil, err
}
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall)
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall, docker)
analyzerService := analyzer.New(opts.ConfigAnalyzer, logger, notifications)
return &daemon{
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
notifications: notifications,
analyzer: analyzerService,
docker: docker,
}, nil
}

View File

@@ -25,5 +25,17 @@
"daemon stopped": "Daemon stopped",
"daemon stop failed": "Daemon stop failed",
"daemon is not running": "Daemon is not running",
"daemon is not reopening logger": "The daemon did not reopen the log"
}
"daemon is not reopening logger": "The daemon did not reopen the log",
"alert.login.ssh.subject": "SSH login alert for user {{.User}} from {{.IP}}",
"alert.login.ssh.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}",
"alert.login.locale.subject": "Login message for user {{.User}} (TTY)",
"alert.login.locale.body": "Logged into the OS via TTY:\n Time: {{.Time}}\n User: {{.User}}\n Log: {{.Log}}",
"alert.login.su.subject": "User {{.ByUser}} has accessed user {{.User}} via su",
"alert.login.su.body": "User {{.ByUser}} accessed user {{.User}} via su.\nTime: {{.Time}}\nLog: {{.Log}}",
"alert.login.sudo.subject": "User {{.ByUser}} has accessed user {{.User}} via sudo",
"alert.login.sudo.body": "User {{.ByUser}} accessed user {{.User}} via sudo.\nTime: {{.Time}}\nLog: {{.Log}}"
}

View File

@@ -25,5 +25,17 @@
"daemon stopped": "Жын тоқтатылды",
"daemon stop failed": "Жынды тоқтату сәтсіз аяқталды",
"daemon is not running": "Демон жұмыс істемейді",
"daemon is not reopening logger": "Жын журналды қайта ашпады"
"daemon is not reopening logger": "Жын журналды қайта ашпады",
"alert.login.ssh.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
"alert.login.ssh.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
"alert.login.locale.subject": "{{.User}} пайдаланушысына арналған кіру хабарламасы (TTY)",
"alert.login.locale.body": "ОЖ-ға TTY арқылы кірдіңіз:\n Уақыт: {{.Time}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
"alert.login.su.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кіру мүмкіндігін алды",
"alert.login.su.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}",
"alert.login.sudo.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кіру мүмкіндігін алды",
"alert.login.sudo.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}"
}

View File

@@ -25,5 +25,17 @@
"daemon stopped": "Демон остановлен",
"daemon stop failed": "Остановка демона не удалась",
"daemon is not running": "Демон не запущен",
"daemon is not reopening logger": "Демон не открыл журнал повторно"
"daemon is not reopening logger": "Демон не открыл журнал повторно",
"alert.login.ssh.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
"alert.login.ssh.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
"alert.login.locale.subject": "Сообщение о входе пользователя {{.User}} (TTY)",
"alert.login.locale.body": "Вошли в ОС через TTY:\n Время: {{.Time}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
"alert.login.su.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su",
"alert.login.su.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su.\nВремя: {{.Time}}\nЛог: {{.Log}}",
"alert.login.sudo.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo",
"alert.login.sudo.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo.\nВремя: {{.Time}}\nЛог: {{.Log}}"
}

View File

@@ -12,6 +12,17 @@ const (
IPv6
)
func (v Version) ToNft() string {
switch v {
case IPv4:
return "ip"
case IPv6:
return "ip6"
default:
return "unknown"
}
}
func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
ipNet, version, err = parseCIDR(ip)
if err != nil {
@@ -21,6 +32,11 @@ func DetermineIPVersion(ip string) (ipNet string, version Version, err error) {
return
}
func IPVersion(ip string) (Version, error) {
_, version, err := parseIP(ip)
return version, err
}
func parseCIDR(parseIP string) (ipNet string, version Version, err error) {
_, parseIPNet, err := net.ParseCIDR(parseIP)
if err != nil {

View File

@@ -0,0 +1,45 @@
package analyzer
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Login Login
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.analyzer"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Login: defaultLogin(),
}
}
func (s Setting) Validate() error {
if err := s.Login.Validate(); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,41 @@
package analyzer
type Login struct {
Enabled bool `mapstructure:"enabled"`
Notify bool `mapstructure:"notify"`
SSHEnable bool `mapstructure:"ssh_enable"`
SSHNotify bool `mapstructure:"ssh_notify"`
LocalEnable bool `mapstructure:"local_enable"`
LocalNotify bool `mapstructure:"local_notify"`
SuEnable bool `mapstructure:"su_enable"`
SuNotify bool `mapstructure:"su_notify"`
SudoEnable bool `mapstructure:"sudo_enable"`
SudoNotify bool `mapstructure:"sudo_notify"`
}
func defaultLogin() Login {
return Login{
Enabled: true,
Notify: true,
SSHEnable: true,
SSHNotify: true,
LocalEnable: true,
LocalNotify: true,
SuEnable: true,
SuNotify: true,
SudoEnable: false,
SudoNotify: true,
}
}
func (l Login) Validate() error {
return nil
}

View File

@@ -1,11 +1,15 @@
package setting
type binaryLocations struct {
Nftables string `mapstructure:"nftables"`
Nftables string `mapstructure:"nftables"`
Journalctl string `mapstructure:"journalctl"`
Docker string `mapstructure:"docker"`
}
func binaryLocationsDefault() *binaryLocations {
return &binaryLocations{
Nftables: "/usr/sbin/nft",
Nftables: "/usr/sbin/nft",
Journalctl: "/bin/journalctl",
Docker: "/usr/bin/docker",
}
}

View File

@@ -0,0 +1,66 @@
package docker
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Enabled bool `mapstructure:"enabled"`
RuleStrategy string `mapstructure:"rule_strategy"`
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.docker"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
if !setting.Enabled {
return setting, nil
}
if err := setting.Validate(); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Enabled: false,
RuleStrategy: "incremental",
}
}
func (s Setting) Validate() error {
return nil
}
func (s Setting) ToRuleStrategy() (docker_monitor.RuleStrategy, error) {
switch s.RuleStrategy {
case "rebuild":
return docker_monitor.RuleStrategyRebuild, nil
case "incremental":
return docker_monitor.RuleStrategyIncremental, nil
}
return docker_monitor.RuleStrategyRebuild, errors.New("invalid option rule_strategy. Must be rebuild or incremental")
}

View File

@@ -4,10 +4,12 @@ import (
"errors"
"strings"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
)
type options struct {
ClearMode string `mapstructure:"clear_mode"`
SavesRules bool `mapstructure:"saves_rules"`
SavesRulesPath string `mapstructure:"saves_rules_path"`
DnsStrict bool `mapstructure:"dns_strict"`
@@ -17,6 +19,7 @@ type options struct {
func defaultOptions() options {
return options{
ClearMode: "global",
SavesRules: false,
SavesRulesPath: "/etc/nftables.conf",
DnsStrict: false,
@@ -45,3 +48,14 @@ func (o options) ValidateSavesRulesPath() error {
return nil
}
func (o options) ToClearMode() (firewall.ClearMode, error) {
switch o.ClearMode {
case "global":
return firewall.ClearModeGlobal, nil
case "own":
return firewall.ClearModeOwn, nil
}
return firewall.ClearModeGlobal, errors.New("invalid option clear_mode. Must be 'global' or 'own'")
}

View File

@@ -11,8 +11,11 @@ type policy struct {
DefaultAllowOutput bool `mapstructure:"default_allow_output"`
DefaultAllowForward bool `mapstructure:"default_allow_forward"`
InputDrop string `mapstructure:"input_drop"`
InputPriority int `mapstructure:"input_priority"`
OutputDrop string `mapstructure:"output_drop"`
OutputPriority int `mapstructure:"output_priority"`
ForwardDrop string `mapstructure:"forward_drop"`
ForwardPriority int `mapstructure:"forward_priority"`
}
func defaultPolicy() policy {
@@ -21,8 +24,11 @@ func defaultPolicy() policy {
DefaultAllowOutput: false,
DefaultAllowForward: false,
InputDrop: "drop",
InputPriority: -10,
OutputDrop: "reject",
OutputPriority: -10,
ForwardDrop: "drop",
ForwardPriority: -10,
}
}
@@ -47,8 +53,11 @@ func (p policy) ToConfigPolicy() (firewall.ConfigPolicy, error) {
DefaultAllowOutput: p.DefaultAllowOutput,
DefaultAllowForward: p.DefaultAllowForward,
InputDrop: inputDrop,
InputPriority: p.InputPriority,
OutputDrop: outputDrop,
OutputPriority: p.OutputPriority,
ForwardDrop: forwardDrop,
ForwardPriority: p.ForwardPriority,
}, nil
}
@@ -70,12 +79,23 @@ func (p policy) Validate() error {
if err := validateDrop(p.InputDrop, "input_drop"); err != nil {
return err
}
if err := validatePriority(p.InputPriority, "input_priority"); err != nil {
return err
}
if err := validateDrop(p.OutputDrop, "output_drop"); err != nil {
return err
}
if err := validatePriority(p.OutputPriority, "output_priority"); err != nil {
return err
}
if err := validateDrop(p.ForwardDrop, "forward_drop"); err != nil {
return err
}
if err := validatePriority(p.ForwardPriority, "forward_priority"); err != nil {
return err
}
return nil
}
@@ -86,3 +106,10 @@ func validateDrop(drop string, parameterName string) error {
}
return fmt.Errorf("invalid %s. Must be drop or reject", parameterName)
}
func validatePriority(priority int, parameterName string) error {
if priority < -50 || priority > 50 {
return fmt.Errorf("%s must be in range -50-50", parameterName)
}
return nil
}

View File

@@ -0,0 +1,129 @@
package notifications
import (
"errors"
"fmt"
"strings"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/wneessen/go-mail"
)
type Email struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
AuthType string `mapstructure:"auth_type"`
TLSMode string `mapstructure:"tls_mode"`
TLSPolicy string `mapstructure:"tls_policy"`
TLSVerify bool `mapstructure:"tls_verify"`
From string `mapstructure:"from"`
To string `mapstructure:"to"`
}
func defaultEmail() Email {
return Email{
Host: "",
Port: 0,
Username: "",
Password: "",
AuthType: "PLAIN",
TLSMode: "STARTTLS",
TLSPolicy: "MANDATORY",
TLSVerify: true,
From: "",
To: "",
}
}
func (e Email) Validate() error {
if e.Host == "" {
return errors.New("host is not specified")
}
if e.Port == 0 {
return errors.New("port is not specified")
}
if err := validate.Port(e.Port, "port"); err != nil {
return err
}
if e.From == "" {
return errors.New("from is not specified")
}
if e.To == "" {
return errors.New("to is not specified")
}
if e.Username == "" {
return errors.New("username is not specified")
}
if e.Password == "" {
return errors.New("password is not specified")
}
return nil
}
func (e Email) ToTLSConfig() (notifications.TLS, error) {
mode, err := parseTLSMode(e.TLSMode)
if err != nil {
return notifications.TLS{}, err
}
policy, err := parseTLSPolicy(e.TLSPolicy)
if err != nil {
return notifications.TLS{}, err
}
return notifications.TLS{
Mode: mode,
Policy: policy,
Verify: e.TLSVerify,
}, nil
}
func ParseAuthType(authType string) (mail.SMTPAuthType, error) {
switch strings.ToUpper(authType) {
case "PLAIN":
return mail.SMTPAuthPlain, nil
case "LOGIN":
return mail.SMTPAuthLogin, nil
case "CRAM-MD5":
return mail.SMTPAuthCramMD5, nil
case "NONE":
return mail.SMTPAuthNoAuth, nil
}
return mail.SMTPAuthNoAuth, fmt.Errorf("unknown auth type: %s", authType)
}
func parseTLSMode(mode string) (notifications.TLSMode, error) {
switch strings.ToUpper(mode) {
case "STARTTLS":
return notifications.TLSModeStartTLS, nil
case "IMPLICIT":
return notifications.TLSModeImplicit, nil
case "NONE":
return notifications.TLSModeNone, nil
}
return notifications.TLSModeNone, fmt.Errorf("unknown tls mode: %s", mode)
}
func parseTLSPolicy(policy string) (notifications.TLSPolicy, error) {
switch strings.ToUpper(policy) {
case "MANDATORY":
return notifications.TLSPolicyMandatory, nil
case "OPPORTUNISTIC":
return notifications.TLSPolicyOpportunistic, nil
}
return notifications.TLSPolicyMandatory, fmt.Errorf("unknown tls policy: %s", policy)
}

View File

@@ -0,0 +1,67 @@
package notifications
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
"github.com/spf13/viper"
)
type Setting struct {
Enabled bool `mapstructure:"enabled"`
ServerName string `mapstructure:"server_name"`
Email Email
}
func InitSetting(path string) (Setting, error) {
if err := validate.IsTomlFile(path, "otherSettingsPath.notifications"); err != nil {
return Setting{}, err
}
setting := settingDefault()
v := viper.New()
v.SetConfigType("toml")
v.SetConfigFile(path)
if err := v.ReadInConfig(); err != nil {
return Setting{}, err
}
if err := v.Unmarshal(&setting); err != nil {
return Setting{}, err
}
if !setting.Enabled {
return setting, nil
}
if err := setting.Validate(); err != nil {
return Setting{}, err
}
return setting, nil
}
func settingDefault() Setting {
return Setting{
Enabled: false,
ServerName: "server",
Email: defaultEmail(),
}
}
func (s Setting) Validate() error {
if s.ServerName == "" {
return errors.New("server_name is not specified")
}
if err := validate.NameWithDot(s.ServerName, "server_name"); err != nil {
return err
}
if err := s.Email.Validate(); err != nil {
return err
}
return nil
}

View File

@@ -1,21 +1,37 @@
package setting
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
analyzerSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/analyzer"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/docker"
firewallSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/firewall"
notificationsSetting "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/notifications"
"github.com/wneessen/go-mail"
)
type otherSettingsPath struct {
Firewall string `mapstructure:"firewall"`
Firewall string `mapstructure:"firewall"`
Notifications string `mapstructure:"notifications"`
Analyzer string `mapstructure:"analyzer"`
Docker string `mapstructure:"docker"`
}
func otherSettingsPathDefault() *otherSettingsPath {
return &otherSettingsPath{
Firewall: "/etc/kor-elf-shield/firewall.toml",
Firewall: "/etc/kor-elf-shield/firewall.toml",
Notifications: "/etc/kor-elf-shield/notifications.toml",
Analyzer: "/etc/kor-elf-shield/analyzer.toml",
Docker: "/etc/kor-elf-shield/docker.toml",
}
}
func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewall.Config, error) {
setting, err := firewallSetting.InitSetting(o.Firewall)
if err != nil {
return firewall.Config{}, err
@@ -36,6 +52,11 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
return firewall.Config{}, err
}
optionClearMode, err := setting.Options.ToClearMode()
if err != nil {
return firewall.Config{}, err
}
return firewall.Config{
InPorts: inPorts,
OutPorts: outPorts,
@@ -55,11 +76,13 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
OutIPs: IPs.OutIP6,
},
Options: firewall.ConfigOptions{
ClearMode: optionClearMode,
SavesRules: setting.Options.SavesRules,
SavesRulesPath: setting.Options.SavesRulesPath,
DnsStrict: setting.Options.DnsStrict,
DnsStrictNs: setting.Options.DnsStrictNs,
PacketFilter: setting.Options.PacketFilter,
DockerSupport: dockerSupport,
},
MetadataNaming: firewall.ConfigMetadata{
TableName: setting.MetadataNaming.TableName,
@@ -70,3 +93,113 @@ func (o *otherSettingsPath) ToFirewallConfig() (firewall.Config, error) {
Policy: configPolicy,
}, nil
}
func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error) {
setting, err := notificationsSetting.InitSetting(o.Notifications)
if err != nil {
return notifications.Config{}, err
}
authType := mail.SMTPAuthPlain
tls := notifications.TLS{}
if setting.Enabled {
authType, err = notificationsSetting.ParseAuthType(setting.Email.AuthType)
if err != nil {
return notifications.Config{}, err
}
tls, err = setting.Email.ToTLSConfig()
if err != nil {
return notifications.Config{}, err
}
}
return notifications.Config{
Enabled: setting.Enabled,
ServerName: setting.ServerName,
Email: notifications.Email{
Host: setting.Email.Host,
Port: uint(setting.Email.Port),
Username: setting.Email.Username,
Password: setting.Email.Password,
AuthType: authType,
TLS: tls,
From: setting.Email.From,
To: setting.Email.To,
},
}, nil
}
func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (config.Config, error) {
if binaryLocations.Journalctl == "" {
return config.Config{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
"Parameter": "binaryLocations.journalctl",
}))
}
setting, err := analyzerSetting.InitSetting(o.Analyzer)
if err != nil {
return config.Config{}, err
}
if err := setting.Validate(); err != nil {
return config.Config{}, err
}
binPath := config.BinPath{
Journalctl: binaryLocations.Journalctl,
}
login := config.Login{
Enabled: setting.Login.Enabled,
Notify: setting.Login.Notify,
SSH: config.LoginSSH{
Enabled: setting.Login.SSHEnable,
Notify: setting.Login.SSHNotify,
},
Local: config.LoginLocal{
Enabled: setting.Login.LocalEnable,
Notify: setting.Login.LocalNotify,
},
Su: config.LoginSu{
Enabled: setting.Login.SuEnable,
Notify: setting.Login.SuNotify,
},
Sudo: config.LoginSudo{
Enabled: setting.Login.SudoEnable,
Notify: setting.Login.SudoNotify,
},
}
return config.Config{
BinPath: binPath,
Login: login,
}, nil
}
func (o *otherSettingsPath) ToDockerConfig(binaryLocations *binaryLocations) (config docker_monitor.Config, dockerSupport bool, err error) {
if binaryLocations.Docker == "" {
return docker_monitor.Config{}, false, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
"Parameter": "binaryLocations.docker",
}))
}
setting, err := docker.InitSetting(o.Docker)
if err != nil {
return docker_monitor.Config{}, false, err
}
if err := setting.Validate(); err != nil {
return docker_monitor.Config{}, false, err
}
ruleStrategy, err := setting.ToRuleStrategy()
if err != nil {
return docker_monitor.Config{}, false, err
}
return docker_monitor.Config{
Path: binaryLocations.Docker,
RuleStrategy: ruleStrategy,
}, setting.Enabled, nil
}

View File

@@ -37,7 +37,7 @@ func settingDefault() *setting {
}
}
func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, error) {
if s.PidFile == "" {
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
"Parameter": "pid_file",
@@ -56,7 +56,12 @@ func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
}))
}
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig()
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
if err != nil {
return daemon.DaemonOptions{}, err
}
analyzerConfig, err := s.OtherSettingsPath.ToAnalyzerConfig(s.BinaryLocations)
if err != nil {
return daemon.DaemonOptions{}, err
}
@@ -66,6 +71,7 @@ func (s setting) ToDaemonOptions() (daemon.DaemonOptions, error) {
PathSocketFile: s.SocketFile,
PathNftables: s.BinaryLocations.Nftables,
ConfigFirewall: firewallConfig,
ConfigAnalyzer: analyzerConfig,
}, nil
}

View File

@@ -54,6 +54,17 @@ func Name(name string, parameterName string) error {
return nil
}
func NameWithDot(name string, parameterName string) error {
if name == "" {
return fmt.Errorf("%s is empty", parameterName)
}
re := regexp.MustCompile(`^[a-zA-Z0-9_\-\.]{1,64}$`)
if !re.MatchString(name) {
return fmt.Errorf("%s must not contain special characters", parameterName)
}
return nil
}
func Port(port int, parameterName string) error {
if port < 0 || port > 65535 {
return fmt.Errorf("%s must be in range 0-65535", parameterName)