diff --git a/overlays/base/etc/modprobe.d/arcline-hardening.conf b/overlays/base/etc/modprobe.d/arcline-hardening.conf new file mode 100644 index 0000000..48c146e --- /dev/null +++ b/overlays/base/etc/modprobe.d/arcline-hardening.conf @@ -0,0 +1,18 @@ +# Arcline OS — disabled kernel modules +# Modules that add attack surface without being needed on the base system. +# The workstation edition keeps most of these disabled too; hardware you +# actually use can be re-enabled per-module. + +# exotic / historically-risky network protocols +blacklist sctp +blacklist dccp +blacklist rds +blacklist tipc + +# unneeded legacy filesystems (mountable by a user with local access) +install cramfs /bin/false +install freevxfs /bin/false +install jffs2 /bin/false +install hfs /bin/false +install hfsplus /bin/false +install udf /bin/false diff --git a/overlays/base/etc/motd b/overlays/base/etc/motd new file mode 100644 index 0000000..4e35e8e --- /dev/null +++ b/overlays/base/etc/motd @@ -0,0 +1,8 @@ + +Welcome to Arcline OS + + Secure by default. Zero telemetry. Self-hosted by principle. + + * hardening guide: man arcline-hardening (or docs/hardening.md) + * manage snapshots: arcline-snapshot snapshot|list|prune + * check firewall: nft list ruleset diff --git a/overlays/base/etc/nftables.conf b/overlays/base/etc/nftables.conf new file mode 100644 index 0000000..c59896a --- /dev/null +++ b/overlays/base/etc/nftables.conf @@ -0,0 +1,44 @@ +#!/usr/sbin/nft -f +# Arcline OS — default-deny firewall (nftables) +# Every edition ships with this policy: drop inbound by default, allow only +# what you opt in to. Services in `accept` rules are the baseline; add yours +# with `nft add rule inet filter input tcp dport accept` (or edit this +# file and reload with `systemctl reload nftables`). + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority filter; policy drop; + + # established traffic is fine + ct state established,related accept + # loopback always + iif "lo" accept + # drop invalid packets early + ct state invalid drop + + # ICMP (needed for PMTU discovery; rate-limited by kernel) + ip protocol icmp accept + ip6 nexthdr icmpv6 accept + + # baseline services + # ssh (key-based auth only — see sshd_config.d) + tcp dport 22 accept + # web (commented out by default; enable when hosting) + # tcp dport 80 accept + # tcp dport 443 accept + } + + chain forward { + type filter hook forward priority filter; policy drop; + # containers route via their own bridge tables (docker/podman); + # this chain stays drop for anything else. + } + + chain output { + type filter hook output priority filter; policy accept; + # zero telemetry means we never call out — but we don't block + # outgoing by default. Arcline never phones home on its own. + } +} diff --git a/overlays/base/etc/ssh/sshd_config.d/10-arcline-hardening.conf b/overlays/base/etc/ssh/sshd_config.d/10-arcline-hardening.conf new file mode 100644 index 0000000..c9184fd --- /dev/null +++ b/overlays/base/etc/ssh/sshd_config.d/10-arcline-hardening.conf @@ -0,0 +1,24 @@ +# Arcline OS — ssh hardening +# Secure by default: key-based auth only, root login via key only. +# To allow password auth, comment the PasswordAuthentication line — you opt +# in to exposure, never out. +# +# Matches Debian's drop-in semantics (Read drop-ins from sshd_config.d). + +# authentication +PermitRootLogin prohibit-password +PubkeyAuthentication yes +PasswordAuthentication no +KbdInteractiveAuthentication no +PermitEmptyPasswords no +MaxAuthTries 3 +LoginGraceTime 30 + +# session hardening +X11Forwarding no +AllowTcpForwarding yes +AllowAgentForwarding yes +ClientAliveInterval 300 +ClientAliveCountMax 2 +TCPKeepAlive no +UseDNS no diff --git a/overlays/base/etc/sysctl.d/10-arcline-hardening.conf b/overlays/base/etc/sysctl.d/10-arcline-hardening.conf new file mode 100644 index 0000000..956f6ad --- /dev/null +++ b/overlays/base/etc/sysctl.d/10-arcline-hardening.conf @@ -0,0 +1,52 @@ +# Arcline OS — kernel hardening (sysctl) +# Applies to every edition via the base overlay. Conservative defaults; +# the workstation edition relaxes a couple of entries (see overlays/workstation). + +# ── memory ────────────────────────────────────────────────────────────────── +vm.swappiness=10 +vm.overcommit_memory=2 +vm.dirty_ratio=5 +vm.dirty_background_ratio=2 + +# ── networking ────────────────────────────────────────────────────────────── +# strict reverse-path filtering +net.ipv4.conf.all.rp_filter=1 +net.ipv4.conf.default.rp_filter=1 +# no ICMP redirects +net.ipv4.conf.all.accept_redirects=0 +net.ipv4.conf.default.accept_redirects=0 +net.ipv4.conf.all.secure_redirects=0 +net.ipv4.conf.default.secure_redirects=0 +net.ipv4.conf.all.send_redirects=0 +net.ipv4.conf.default.send_redirects=0 +net.ipv6.conf.all.accept_redirects=0 +net.ipv6.conf.default.accept_redirects=0 +# ICMP hardening +net.ipv4.icmp_echo_ignore_broadcasts=1 +net.ipv4.icmp_ignore_bogus_error_responses=1 +# TCP hardening +net.ipv4.tcp_syncookies=1 +net.ipv4.tcp_rfc1337=1 +net.ipv4.tcp_timestamps=0 +net.ipv4.conf.all.log_martians=1 +net.ipv4.conf.default.log_martians=1 +# IPv6: don't auto-accept router advertisements +net.ipv6.conf.all.accept_ra=0 +net.ipv6.conf.default.accept_ra=0 + +# ── kernel / visibility ───────────────────────────────────────────────────── +kernel.dmesg_restrict=1 +kernel.kptr_restrict=2 +kernel.perf_event_paranoid=3 +kernel.yama.ptrace_scope=1 +kernel.core_uses_pid=1 +kernel.unprivileged_bpf_disabled=1 +kernel.randomize_va_space=2 +kernel.sysrq=4 + +# ── filesystem hardening ──────────────────────────────────────────────────── +fs.protected_hardlinks=1 +fs.protected_symlinks=1 +fs.protected_fifos=2 +fs.protected_regular=2 +fs.suid_dumpable=0 diff --git a/overlays/base/etc/systemd/journald.conf.d/10-arcline.conf b/overlays/base/etc/systemd/journald.conf.d/10-arcline.conf new file mode 100644 index 0000000..5f7eac6 --- /dev/null +++ b/overlays/base/etc/systemd/journald.conf.d/10-arcline.conf @@ -0,0 +1,12 @@ +# Arcline OS — journald +# Persistent, bounded, compressed logs. Logs live on @log so they survive +# system rollbacks (see btrfs/README.md). + +[Journal] +Storage=persistent +Compress=yes +SystemMaxUse=500M +SystemMaxFileSize=100M +MaxRetentionSec=14day +ForwardToSyslog=no +ForwardToConsole=no diff --git a/overlays/base/etc/systemd/system.conf.d/10-arcline.conf b/overlays/base/etc/systemd/system.conf.d/10-arcline.conf new file mode 100644 index 0000000..a0ed68e --- /dev/null +++ b/overlays/base/etc/systemd/system.conf.d/10-arcline.conf @@ -0,0 +1,9 @@ +# Arcline OS — systemd manager hardening +# Global defaults applied to every unit. No core dumps on disk, sane limits. + +[Manager] +DefaultLimitNOFILE=65535 +DefaultLimitNPROC=4096 +DefaultLimitCORE=0 +DumpCore=no +CoreDumpSize=0 diff --git a/overlays/base/usr/lib/systemd/system/arcline-snapshot.service b/overlays/base/usr/lib/systemd/system/arcline-snapshot.service new file mode 100644 index 0000000..e65bafb --- /dev/null +++ b/overlays/base/usr/lib/systemd/system/arcline-snapshot.service @@ -0,0 +1,12 @@ +[Unit] +Description=Arcline btrfs snapshot +Documentation=file:///usr/local/sbin/arcline-snapshot +RequiresMountsFor=/.snapshots +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/arcline-snapshot snapshot + +[Install] +WantedBy=multi-user.target diff --git a/overlays/base/usr/lib/systemd/system/arcline-snapshot.timer b/overlays/base/usr/lib/systemd/system/arcline-snapshot.timer new file mode 100644 index 0000000..5e49906 --- /dev/null +++ b/overlays/base/usr/lib/systemd/system/arcline-snapshot.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Daily Arcline btrfs snapshot +Requires=arcline-snapshot.service + +[Timer] +OnCalendar=*-*-* 03:00:00 +Persistent=true +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/overlays/cloud/etc/cloud/cloud.cfg b/overlays/cloud/etc/cloud/cloud.cfg new file mode 100644 index 0000000..8aa29cb --- /dev/null +++ b/overlays/cloud/etc/cloud/cloud.cfg @@ -0,0 +1,41 @@ +# Arcline OS — cloud-init configuration (cloud edition) +# Minimal, deterministic first-boot provisioning. No telemetry: cloud-init is +# pointed only at the configured datasource and never phones home. + +# hostname +preserve_hostname: false +hostname: arclines +fqdn: arclines.local + +# datasource: auto-detected (aws, gce, azure, openstack, hetzner, ...) +datasource_list: [ NoCloud, ConfigDrive, OpenStack, Ec2, GCE, Azure, Hetzner, None ] +datasource: + Ec2: + strict_id: false + metadata_urls: ["http://169.254.169.254"] + +# only the arcline user is guaranteed; everything else comes from user-data +system_info: + default_user: + name: arcline + lock_passwd: true + gecos: Arcline Cloud User + groups: [adm, sudo, systemd-journal] + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + shell: /bin/bash + +# disable cloud-init's own telemetry/reporting +reporting: + logging: + type: log + +# grow root partition on first boot (cloud volumes) +growpart: + mode: auto + devices: ["/"] + +resize_rootfs: true + +# keep ssh config managed by Arcline, not cloud-init +ssh_genkeytypes: ["ed25519"] +disable_ec2_metadata: false diff --git a/overlays/server/etc/grafana/provisioning/dashboards/dashboards.yml b/overlays/server/etc/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..fa1664f --- /dev/null +++ b/overlays/server/etc/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,13 @@ +# Arcline OS — Grafana dashboard provisioning (server edition) +apiVersion: 1 + +providers: + - name: Arcline + orgId: 1 + folder: Arcline + type: file + disableDeletion: false + allowUiUpdates: true + updateIntervalSeconds: 30 + options: + path: /etc/grafana/provisioning/dashboards diff --git a/overlays/server/etc/grafana/provisioning/dashboards/node-overview.json b/overlays/server/etc/grafana/provisioning/dashboards/node-overview.json new file mode 100644 index 0000000..c96a1d2 --- /dev/null +++ b/overlays/server/etc/grafana/provisioning/dashboards/node-overview.json @@ -0,0 +1,94 @@ +{ + "uid": "arcline-node-overview", + "title": "Arcline Node Overview", + "tags": ["arcline", "node"], + "timezone": "browser", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-1h", "to": "now" }, + "panels": [ + { + "id": 1, + "title": "System load", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "node_load1", + "legendFormat": "load1", + "refId": "A" + }, + { + "expr": "node_load5", + "legendFormat": "load5", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": { "unit": "short" }, + "overrides": [] + } + }, + { + "id": 2, + "title": "Memory used", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100", + "legendFormat": "% used", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { "unit": "percent", "max": 100, "min": 0 }, + "overrides": [] + } + }, + { + "id": 3, + "title": "Disk usage", + "type": "gauge", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "(1 - node_filesystem_avail_bytes{mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"}) * 100", + "legendFormat": "/", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { "unit": "percent", "max": 100, "min": 0 }, + "overrides": [] + } + }, + { + "id": 4, + "title": "Network traffic", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "datasource": { "type": "prometheus", "uid": "Prometheus" }, + "targets": [ + { + "expr": "rate(node_network_receive_bytes_total[5m])", + "legendFormat": "rx {{ $labels.device }}", + "refId": "A" + }, + { + "expr": "rate(node_network_transmit_bytes_total[5m])", + "legendFormat": "tx {{ $labels.device }}", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": { "unit": "Bps" }, + "overrides": [] + } + } + ] +} diff --git a/overlays/server/etc/grafana/provisioning/datasources/prometheus.yml b/overlays/server/etc/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..cd16a19 --- /dev/null +++ b/overlays/server/etc/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,20 @@ +# Arcline OS — Grafana datasource provisioning (server edition) +# Grafana is installed from the upstream apt repo (see configure-system.sh / +# docs/observability.md). These files auto-provision on first start. + +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://localhost:9090 + isDefault: true + editable: false + + - name: Loki + type: loki + access: proxy + url: http://localhost:3100 + isDefault: false + editable: false diff --git a/overlays/server/etc/loki/loki.yml b/overlays/server/etc/loki/loki.yml new file mode 100644 index 0000000..c5ad494 --- /dev/null +++ b/overlays/server/etc/loki/loki.yml @@ -0,0 +1,35 @@ +# Arcline OS — Loki configuration (server edition) +# Single-node, filesystem storage, 14-day retention. Loki is installed from +# the upstream tarball (see docs/observability.md). + +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9095 + +common: + path_prefix: /var/lib/loki + storage: + filesystem: + chunks_directory: /var/lib/loki/chunks + rules_directory: /var/lib/loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +limits_config: + retention_period: 14d + allow_structured_metadata: false diff --git a/overlays/server/etc/prometheus/arcline.rules.yml b/overlays/server/etc/prometheus/arcline.rules.yml new file mode 100644 index 0000000..d043229 --- /dev/null +++ b/overlays/server/etc/prometheus/arcline.rules.yml @@ -0,0 +1,43 @@ +# Arcline OS — pre-built alerting rules (Prometheus) +groups: + - name: arcline-node + rules: + - alert: NodeDown + expr: up == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "{{ $labels.instance }} is unreachable" + + - alert: HighCpuLoad + expr: node_load1 / count(node_cpu_seconds_total{mode="idle"}) > 2 + for: 10m + labels: + severity: warning + annotations: + summary: "sustained load average > 2 on {{ $labels.instance }}" + + - alert: DiskAlmostFull + expr: (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes) * 100 > 85 + for: 10m + labels: + severity: warning + annotations: + summary: "filesystem {{ $labels.mountpoint }} on {{ $labels.instance }} > 85%" + + - alert: RootDiskCritical + expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 > 95 + for: 5m + labels: + severity: critical + annotations: + summary: "root filesystem on {{ $labels.instance }} > 95%" + + - alert: ServiceRestarts + expr: increase(node_systemd_unit_restart_total[15m]) > 2 + for: 0m + labels: + severity: warning + annotations: + summary: "unit {{ $labels.name }} restarting repeatedly on {{ $labels.instance }}" diff --git a/overlays/server/etc/prometheus/prometheus.yml b/overlays/server/etc/prometheus/prometheus.yml new file mode 100644 index 0000000..c88bbee --- /dev/null +++ b/overlays/server/etc/prometheus/prometheus.yml @@ -0,0 +1,25 @@ +# Arcline OS — Prometheus configuration (server edition) +# Scrapes itself and node_exporter on the local host. Extension points for +# the Arcline toolchain (arcline-uptime exposes :8081/metrics) are commented. + +global: + scrape_interval: 15s + evaluation_interval: 15s + external_labels: + cluster: arcline + +rule_files: + - /etc/prometheus/arcline.rules.yml + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + + - job_name: node + static_configs: + - targets: ["localhost:9100"] + + # - job_name: arcline-uptime + # static_configs: + # - targets: ["localhost:8081"] diff --git a/overlays/server/etc/promtail/promtail.yml b/overlays/server/etc/promtail/promtail.yml new file mode 100644 index 0000000..88b461d --- /dev/null +++ b/overlays/server/etc/promtail/promtail.yml @@ -0,0 +1,31 @@ +# Arcline OS — Promtail configuration (server edition) +# Ships systemd journal + /var/log files to Loki. Zero telemetry: this only +# talks to the local Loki instance on localhost. + +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /var/lib/promtail/positions.yaml + +clients: + - url: http://localhost:3100/loki/api/v1/push + +scrape_configs: + - job_name: systemd-journal + journal: + path: /var/log/journal + max_age: 12h + labels: + job: systemd-journal + relabel_configs: + - source_labels: ["__journal__systemd_unit"] + target_label: "unit" + + - job_name: varlogs + static_configs: + - targets: [localhost] + labels: + job: varlogs + __path__: /var/log/**/*.log diff --git a/overlays/workstation/etc/profile.d/arcline-dev.sh b/overlays/workstation/etc/profile.d/arcline-dev.sh new file mode 100644 index 0000000..abd9df6 --- /dev/null +++ b/overlays/workstation/etc/profile.d/arcline-dev.sh @@ -0,0 +1,12 @@ +# Arcline Workstation — developer environment (profile.d) +# Standard Go / Rust / Node paths for every login shell. + +# Go +export GOPATH="$HOME/go" +export PATH="$PATH:/usr/local/go/bin:$GOPATH/bin" + +# Rust (rustup installs to ~/.cargo) +export PATH="$PATH:$HOME/.cargo/bin" + +# Node +export PATH="$PATH:./node_modules/.bin" diff --git a/overlays/workstation/etc/sysctl.d/90-arcline-desktop.conf b/overlays/workstation/etc/sysctl.d/90-arcline-desktop.conf new file mode 100644 index 0000000..d0f274a --- /dev/null +++ b/overlays/workstation/etc/sysctl.d/90-arcline-desktop.conf @@ -0,0 +1,12 @@ +# Arcline Workstation — desktop relaxation overrides +# The base hardening is strict (perf_event_paranoid=3 blocks `perf` for +# unprivileged users). A workstation developer wants perf, tracing, and +# unprivileged user namespaces (used by podman/rootless containers). These +# lines land AFTER the base file, so they win. + +kernel.perf_event_paranoid=1 +kernel.unprivileged_bpf_disabled=0 +kernel.yama.ptrace_scope=0 + +# rootless containers +kernel.unprivileged_userns_clone=1