feat: add hardened base and per-edition image overlays
Add the files that land in the image, organised as layered rootfs trees (base first, then the edition layer wins on conflict). - base: hardened kernel cmdline + sysctl, default-deny nftables, key-only ssh, persistent journald, module blacklist, no core dumps, snapshot timer units, motd. - server: Prometheus + auto-provisioned Grafana + Loki + promtail. - workstation: dev profile and desktop sysctl relaxations (perf, rootless containers). - cloud: cloud-init provisioning config.
This commit is contained in:
18
overlays/base/etc/modprobe.d/arcline-hardening.conf
Normal file
18
overlays/base/etc/modprobe.d/arcline-hardening.conf
Normal file
@@ -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
|
||||
8
overlays/base/etc/motd
Normal file
8
overlays/base/etc/motd
Normal file
@@ -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
|
||||
44
overlays/base/etc/nftables.conf
Normal file
44
overlays/base/etc/nftables.conf
Normal file
@@ -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 <port> 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.
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
52
overlays/base/etc/sysctl.d/10-arcline-hardening.conf
Normal file
52
overlays/base/etc/sysctl.d/10-arcline-hardening.conf
Normal file
@@ -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
|
||||
12
overlays/base/etc/systemd/journald.conf.d/10-arcline.conf
Normal file
12
overlays/base/etc/systemd/journald.conf.d/10-arcline.conf
Normal file
@@ -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
|
||||
9
overlays/base/etc/systemd/system.conf.d/10-arcline.conf
Normal file
9
overlays/base/etc/systemd/system.conf.d/10-arcline.conf
Normal file
@@ -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
|
||||
@@ -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
|
||||
11
overlays/base/usr/lib/systemd/system/arcline-snapshot.timer
Normal file
11
overlays/base/usr/lib/systemd/system/arcline-snapshot.timer
Normal file
@@ -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
|
||||
41
overlays/cloud/etc/cloud/cloud.cfg
Normal file
41
overlays/cloud/etc/cloud/cloud.cfg
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
35
overlays/server/etc/loki/loki.yml
Normal file
35
overlays/server/etc/loki/loki.yml
Normal file
@@ -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
|
||||
43
overlays/server/etc/prometheus/arcline.rules.yml
Normal file
43
overlays/server/etc/prometheus/arcline.rules.yml
Normal file
@@ -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 }}"
|
||||
25
overlays/server/etc/prometheus/prometheus.yml
Normal file
25
overlays/server/etc/prometheus/prometheus.yml
Normal file
@@ -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"]
|
||||
31
overlays/server/etc/promtail/promtail.yml
Normal file
31
overlays/server/etc/promtail/promtail.yml
Normal file
@@ -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
|
||||
12
overlays/workstation/etc/profile.d/arcline-dev.sh
Normal file
12
overlays/workstation/etc/profile.d/arcline-dev.sh
Normal file
@@ -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"
|
||||
12
overlays/workstation/etc/sysctl.d/90-arcline-desktop.conf
Normal file
12
overlays/workstation/etc/sysctl.d/90-arcline-desktop.conf
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user