You've already forked caddy-opnsense-blocker
Add MIT license and Nix packaging
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/data/
|
/data/
|
||||||
/caddy-opnsense-blocker
|
/caddy-opnsense-blocker
|
||||||
|
/result
|
||||||
|
/result-*
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Richard Dern
|
||||||
|
|
||||||
|
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.
|
||||||
56
README.md
56
README.md
@@ -35,6 +35,10 @@ This keeps the application usable immediately while leaving room for a more adva
|
|||||||
- `internal/service`: runs concurrent log followers and applies automatic decisions
|
- `internal/service`: runs concurrent log followers and applies automatic decisions
|
||||||
- `internal/web`: serves the local review UI and JSON API
|
- `internal/web`: serves the local review UI and JSON API
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See `LICENSE`.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Generate or provision OPNsense API credentials.
|
1. Generate or provision OPNsense API credentials.
|
||||||
@@ -88,6 +92,58 @@ CGO_ENABLED=0 go build ./cmd/caddy-opnsense-blocker
|
|||||||
|
|
||||||
`CGO_ENABLED=0` is useful on systems without a C toolchain. The application itself only relies on pure-Go dependencies.
|
`CGO_ENABLED=0` is useful on systems without a C toolchain. The application itself only relies on pure-Go dependencies.
|
||||||
|
|
||||||
|
## Nix packaging
|
||||||
|
|
||||||
|
The repository ships with first-class Nix files:
|
||||||
|
|
||||||
|
- `package.nix`: reusable package definition
|
||||||
|
- `default.nix`: convenience entry point for `nix-build`
|
||||||
|
- `module.nix`: reusable NixOS module
|
||||||
|
|
||||||
|
Build the package directly from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the NixOS module from another configuration:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
imports = [ /path/to/caddy-opnsense-blocker/module.nix ];
|
||||||
|
|
||||||
|
services.caddy-opnsense-blocker = {
|
||||||
|
enable = true;
|
||||||
|
credentials.opnsenseApiKeyFile = "/run/secrets/opnsense-api-key";
|
||||||
|
credentials.opnsenseApiSecretFile = "/run/secrets/opnsense-api-secret";
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
opnsense = {
|
||||||
|
enabled = true;
|
||||||
|
base_url = "https://router.example.test";
|
||||||
|
ensure_alias = true;
|
||||||
|
alias.name = "blocked-ips";
|
||||||
|
};
|
||||||
|
|
||||||
|
profiles.public-web = {
|
||||||
|
auto_block = true;
|
||||||
|
block_unexpected_posts = true;
|
||||||
|
block_php_paths = true;
|
||||||
|
suspicious_path_prefixes = [ "/wp-admin" "/wp-login.php" "/.env" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
{
|
||||||
|
name = "public-web";
|
||||||
|
path = "/var/log/caddy/public-web.json";
|
||||||
|
profile = "public-web";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- richer decision engine
|
- richer decision engine
|
||||||
|
|||||||
3
default.nix
Normal file
3
default.nix
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
let
|
||||||
|
pkgs = import <nixpkgs> { };
|
||||||
|
in pkgs.callPackage ./package.nix { }
|
||||||
158
module.nix
Normal file
158
module.nix
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.caddy-opnsense-blocker;
|
||||||
|
yamlFormat = pkgs.formats.yaml { };
|
||||||
|
credentialsDirectory = "/run/credentials/caddy-opnsense-blocker.service";
|
||||||
|
|
||||||
|
credentialSettings = lib.optionalAttrs (cfg.credentials.opnsenseApiKeyFile != null) {
|
||||||
|
opnsense.api_key_file = "${credentialsDirectory}/opnsense_api_key";
|
||||||
|
} // lib.optionalAttrs (cfg.credentials.opnsenseApiSecretFile != null) {
|
||||||
|
opnsense.api_secret_file = "${credentialsDirectory}/opnsense_api_secret";
|
||||||
|
};
|
||||||
|
|
||||||
|
defaultSettings = {
|
||||||
|
server = {
|
||||||
|
listen_address = "127.0.0.1:9080";
|
||||||
|
read_timeout = "5s";
|
||||||
|
write_timeout = "10s";
|
||||||
|
shutdown_timeout = "15s";
|
||||||
|
};
|
||||||
|
|
||||||
|
storage.path = "/var/lib/${cfg.stateDirectoryName}/caddy-opnsense-blocker.db";
|
||||||
|
};
|
||||||
|
|
||||||
|
mergedSettings = lib.recursiveUpdate defaultSettings (lib.recursiveUpdate credentialSettings cfg.settings);
|
||||||
|
configFile = yamlFormat.generate "caddy-opnsense-blocker.yaml" mergedSettings;
|
||||||
|
|
||||||
|
loadCredential =
|
||||||
|
lib.optional (cfg.credentials.opnsenseApiKeyFile != null)
|
||||||
|
"opnsense_api_key:${toString cfg.credentials.opnsenseApiKeyFile}"
|
||||||
|
++ lib.optional (cfg.credentials.opnsenseApiSecretFile != null)
|
||||||
|
"opnsense_api_secret:${toString cfg.credentials.opnsenseApiSecretFile}";
|
||||||
|
|
||||||
|
opnsenseEnabled = lib.attrByPath [ "opnsense" "enabled" ] false mergedSettings;
|
||||||
|
hasOPNsenseAPIKey =
|
||||||
|
cfg.credentials.opnsenseApiKeyFile != null
|
||||||
|
|| lib.hasAttrByPath [ "opnsense" "api_key" ] cfg.settings
|
||||||
|
|| lib.hasAttrByPath [ "opnsense" "api_key_file" ] cfg.settings;
|
||||||
|
hasOPNsenseAPISecret =
|
||||||
|
cfg.credentials.opnsenseApiSecretFile != null
|
||||||
|
|| lib.hasAttrByPath [ "opnsense" "api_secret" ] cfg.settings
|
||||||
|
|| lib.hasAttrByPath [ "opnsense" "api_secret_file" ] cfg.settings;
|
||||||
|
in {
|
||||||
|
options.services.caddy-opnsense-blocker = {
|
||||||
|
enable = lib.mkEnableOption "caddy-opnsense-blocker";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = pkgs.callPackage ./package.nix { };
|
||||||
|
defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }";
|
||||||
|
description = "Package used to run caddy-opnsense-blocker.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "caddy";
|
||||||
|
description = "User account used by the service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "caddy";
|
||||||
|
description = "Primary group used by the service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDirectoryName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "caddy-opnsense-blocker";
|
||||||
|
description = "Systemd state directory name for the service.";
|
||||||
|
};
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
opnsenseApiKeyFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to the OPNsense API key loaded through systemd credentials.";
|
||||||
|
};
|
||||||
|
|
||||||
|
opnsenseApiSecretFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to the OPNsense API secret loaded through systemd credentials.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = lib.mkOption {
|
||||||
|
type = yamlFormat.type;
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
opnsense = {
|
||||||
|
enabled = true;
|
||||||
|
base_url = "https://router.example.test";
|
||||||
|
ensure_alias = true;
|
||||||
|
alias.name = "blocked-ips";
|
||||||
|
};
|
||||||
|
|
||||||
|
profiles.public-web = {
|
||||||
|
auto_block = true;
|
||||||
|
block_unexpected_posts = true;
|
||||||
|
suspicious_path_prefixes = [ "/wp-admin" "/wp-login.php" "/.env" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
{
|
||||||
|
name = "public-web";
|
||||||
|
path = "/var/log/caddy/public-web.json";
|
||||||
|
profile = "public-web";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
description = "YAML-equivalent application settings written to a generated runtime configuration file.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = (cfg.credentials.opnsenseApiKeyFile == null) == (cfg.credentials.opnsenseApiSecretFile == null);
|
||||||
|
message = "services.caddy-opnsense-blocker.credentials.opnsenseApiKeyFile and opnsenseApiSecretFile must either both be set or both be null.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = !opnsenseEnabled || (hasOPNsenseAPIKey && hasOPNsenseAPISecret);
|
||||||
|
message = "services.caddy-opnsense-blocker requires OPNsense credentials when settings.opnsense.enabled = true.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.caddy-opnsense-blocker = {
|
||||||
|
description = "Caddy OPNsense blocker";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
StateDirectory = cfg.stateDirectoryName;
|
||||||
|
WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
|
||||||
|
ExecStart = "${cfg.package}/bin/caddy-opnsense-blocker -config ${configFile}";
|
||||||
|
LoadCredential = loadCredential;
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "5s";
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
20
package.nix
Normal file
20
package.nix
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{ buildGoModule, lib }:
|
||||||
|
|
||||||
|
buildGoModule {
|
||||||
|
pname = "caddy-opnsense-blocker";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
|
subPackages = [ "cmd/caddy-opnsense-blocker" ];
|
||||||
|
vendorHash = "sha256-xS1nuEjnpkKbmresj35UtNOps0dotgPCQn/bjRYp8Xk=";
|
||||||
|
env.CGO_ENABLED = 0;
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Real-time Caddy log ingestion with manual review and OPNsense blocking";
|
||||||
|
homepage = "https://git.dern.ovh/infrastructure/caddy-opnsense-blocker";
|
||||||
|
license = licenses.mit;
|
||||||
|
mainProgram = "caddy-opnsense-blocker";
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user