Compare commits

..

9 commits

Author SHA1 Message Date
f1b6be6f3f Add fail2ban configuration for SSH and Forgejo on Trantor
- Configure fail2ban with progressive ban times (1h base, up to 10000h max)
- Add SSH jail with password authentication disabled
- Add Forgejo jail using systemd journal backend
- Ignore private networks and Tailscale IPs
- Set Forgejo to 10 retries per hour, 15min initial ban

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 13:00:17 -03:00
cd17bf2561 only forgejo is public for now 2025-11-09 12:36:43 -03:00
ad9d565a8f Route DNS based on service visibility flags
Replace wildcard DNS with dynamic service-based routing that reads
from shared/services.nix. Public services (forgejo, vaultwarden,
nextcloud) point to trantor's public IP for external access, while
private services (kanidm, jellyfin) point to tailscale IPs for
internal-only access. This provides granular control over service
exposure without manual DNS management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 11:20:21 -03:00
878c4aa3ea Add public visibility flags to service definitions
Mark services as public or private to control external access:
- Public: vaultwarden, forgejo, nextcloud
- Private: kanidm, jellyfin

This enables proper routing and firewall configuration based on
intended service visibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 11:12:06 -03:00
6f1aca7b01 Configure Forgejo OAuth2 and disable public registration
Add OAuth2 client configuration to enable auto-registration via SSO
with Kanidm, while disabling direct public registration. Users can now
authenticate through the identity provider with automatic account
creation and avatar syncing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 11:11:55 -03:00
14c4440dd1 forgejo: disable singup; document root password 2025-11-09 10:34:57 -03:00
1921aad1bd Update Cloudflare DNS configuration with explicit zone ID
Replace sensitive zone_id variable with hardcoded value and update
DNS record configuration to use cloudflare_dns_record resource type.
Disable proxying and set explicit TTL for better control over DNS
propagation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 10:30:02 -03:00
1b1d7896e6 Document required environment variables for OCI configuration
Add documentation about required OCI and AWS credentials for the
trantor configuration, clarifying that ~/.oci/config can be used
as an alternative to environment variables.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 10:29:54 -03:00
808bccf0a2 Add Tailscale tailnet DNS configuration via Terranix
Configure global DNS nameservers for the Tailscale tailnet, setting
trantor as the primary DNS server with Cloudflare as fallback. This
enables custom DNS resolution across the entire tailnet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 10:29:45 -03:00
10 changed files with 277 additions and 18 deletions

View file

@ -0,0 +1,43 @@
{ config, pkgs, ... }:
{
services.fail2ban = {
enable = true;
maxretry = 5;
ignoreIP = [
"127.0.0.0/8"
"::1"
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/16"
"100.64.0.0/10"
];
bantime = "1h";
bantime-increment = {
enable = true;
multipliers = "1 2 4 8 16 32 64";
maxtime = "10000h";
overalljails = true;
};
jails.forgejo = {
settings = {
enabled = true;
filter = "forgejo";
backend = "systemd";
maxretry = 10;
findtime = "1h";
bantime = "15m";
};
};
};
# Custom fail2ban filter for Forgejo using systemd journal
environment.etc."fail2ban/filter.d/forgejo.local".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
[Definition]
journalmatch = _SYSTEMD_UNIT=forgejo.service
failregex = Failed authentication attempt for .+ from <HOST>:\d+:
ignoreregex =
'');
}

View file

@ -9,7 +9,8 @@ let
inherit (utils) mkNginxVHosts;
in
{
services.forgejo = {
services = {
forgejo = {
enable = true;
repositoryRoot = "/data/forgejo";
settings = {
@ -24,11 +25,34 @@ in
log.LEVEL = "Warn";
mailer.ENABLED = false;
actions.ENABLED = false;
service.DISABLE_REGISTRATION = true;
oauth2_client = {
ENABLE_AUTO_REGISTRATION = true;
UPDATE_AVATAR = true;
ACCOUNT_LINKING = "login";
USERNAME = "preferred_username";
};
};
services.nginx.virtualHosts = mkNginxVHosts {
};
nginx.virtualHosts = mkNginxVHosts {
domains."git.baduhai.dev".locations."/".proxyPass =
"http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}:/";
};
fail2ban.jails.forgejo = {
settings = {
enabled = true;
filter = "forgejo";
logpath = "${config.services.forgejo.stateDir}/log/forgejo.log";
maxretry = 10;
findtime = "1h";
bantime = "15m";
};
};
};
environment.etc."fail2ban/filter.d/forgejo.conf".text = ''
[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
'';
}

23
hosts/trantor/openssh.nix Normal file
View file

@ -0,0 +1,23 @@
{ ... }:
{
services = {
openssh = {
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
};
fail2ban.jails.sshd = {
settings = {
enabled = true;
port = "ssh";
filter = "sshd";
logpath = "/var/log/auth.log";
maxretry = 5;
findtime = "10m";
bantime = "1h";
};
};
};
}

Binary file not shown.

View file

@ -27,4 +27,9 @@ in
rotterdam-user
alexandria
];
"forgejo-root-password.age".publicKeys = [
io-user
rotterdam-user
trantor
];
}

View file

@ -24,6 +24,7 @@
name = "forgejo";
domain = "git.baduhai.dev";
host = "trantor";
public = true;
tailscaleIP = "100.108.5.90";
port = 3000;
}

View file

@ -0,0 +1,102 @@
# Required environment variables:
# CLOUDFLARE_API_TOKEN - API token with "Edit zone DNS" permissions
# AWS_ACCESS_KEY_ID - Cloudflare R2 access key for state storage
# AWS_SECRET_ACCESS_KEY - Cloudflare R2 secret key for state storage
{ config, lib, ... }:
let
inherit (import ../../shared/services.nix) services;
# Helper to extract subdomain from full domain (e.g., "git.baduhai.dev" -> "git")
getSubdomain = domain: lib.head (lib.splitString "." domain);
# Generate DNS records for services
# Public services point to trantor's public IP
# Private services point to their tailscale IP
mkServiceRecords = lib.listToAttrs (
lib.imap0 (i: svc:
let
subdomain = getSubdomain svc.domain;
targetIP = if svc.public or false
then config.data.terraform_remote_state.trantor "outputs.instance_public_ip"
else svc.tailscaleIP;
in {
name = "service_${toString i}";
value = {
zone_id = config.variable.zone_id.default;
name = subdomain;
type = "A";
content = targetIP;
proxied = false;
ttl = 3600;
};
}
) services
);
in
{
terraform.required_providers.cloudflare = {
source = "cloudflare/cloudflare";
version = "~> 5.0";
};
terraform.backend.s3 = {
bucket = "terraform-state";
key = "cloudflare/baduhai.dev.tfstate";
region = "auto";
endpoint = "https://fcdf920bde00c3d013ee541f984da70e.r2.cloudflarestorage.com";
skip_credentials_validation = true;
skip_metadata_api_check = true;
skip_region_validation = true;
skip_requesting_account_id = true;
use_path_style = true;
};
variable = {
zone_id = {
default = "c63a8332fdddc4a8e5612ddc54557044";
type = "string";
};
};
data = {
terraform_remote_state.trantor = {
backend = "s3";
config = {
bucket = "terraform-state";
key = "oci/trantor.tfstate";
region = "auto";
endpoint = "https://fcdf920bde00c3d013ee541f984da70e.r2.cloudflarestorage.com";
skip_credentials_validation = true;
skip_metadata_api_check = true;
skip_region_validation = true;
skip_requesting_account_id = true;
use_path_style = true;
};
};
};
resource = {
cloudflare_dns_record = mkServiceRecords // {
root = {
zone_id = config.variable.zone_id.default;
name = "@";
type = "A";
content = config.data.terraform_remote_state.trantor "outputs.instance_public_ip";
proxied = false;
ttl = 3600;
};
www = {
zone_id = config.variable.zone_id.default;
name = "www";
type = "A";
content = config.data.terraform_remote_state.trantor "outputs.instance_public_ip";
proxied = false;
ttl = 3600;
};
};
};
}

View file

@ -1,3 +1,13 @@
# Required environment variables:
# instead of OCI variables, ~/.oci/config may also be used
# OCI_TENANCY_OCID - Oracle tenancy OCID (or use TF_VAR_* to override variables)
# OCI_USER_OCID - Oracle user OCID
# OCI_FINGERPRINT - API key fingerprint
# OCI_PRIVATE_KEY_PATH - Path to OCI API private key
# AWS variables are required
# AWS_ACCESS_KEY_ID - Cloudflare R2 access key for state storage
# AWS_SECRET_ACCESS_KEY - Cloudflare R2 secret key for state storage
{ config, ... }:
{

View file

@ -0,0 +1,43 @@
# Required environment variables:
# TAILSCALE_API_KEY - Tailscale API key with appropriate permissions
# TAILSCALE_TAILNET - Your tailnet name (e.g., "user@example.com" or "example.org.github")
# AWS_ACCESS_KEY_ID - Cloudflare R2 access key for state storage
# AWS_SECRET_ACCESS_KEY - Cloudflare R2 secret key for state storage
{ config, ... }:
{
terraform.required_providers.tailscale = {
source = "tailscale/tailscale";
version = "~> 0.17";
};
terraform.backend.s3 = {
bucket = "terraform-state";
key = "tailscale/tailnet.tfstate";
region = "auto";
endpoint = "https://fcdf920bde00c3d013ee541f984da70e.r2.cloudflarestorage.com";
skip_credentials_validation = true;
skip_metadata_api_check = true;
skip_region_validation = true;
skip_requesting_account_id = true;
use_path_style = true;
};
variable = {
trantor_tailscale_ip = {
default = "100.108.5.90";
type = "string";
};
};
resource = {
tailscale_dns_nameservers.global = {
nameservers = [
config.variable.trantor_tailscale_ip.default
"1.1.1.1"
"1.0.0.1"
];
};
};
}

View file

@ -14,6 +14,14 @@
modules = [ ./terranix/oci/trantor.nix ];
terraformWrapper.package = pkgs.opentofu;
};
cloudflare-baduhaidev = {
modules = [ ./terranix/cloudflare/baduhai.dev.nix ];
terraformWrapper.package = pkgs.opentofu;
};
tailscale-tailnet = {
modules = [ ./terranix/tailscale/tailnet.nix ];
terraformWrapper.package = pkgs.opentofu;
};
};
};
}