From 927b5b493452ea4789518c4c6fe10a37cc55b69e Mon Sep 17 00:00:00 2001 From: il Date: Wed, 17 Jun 2026 12:05:05 +0900 Subject: [PATCH] feat(repo): IaaS baseline --- data/volumes/.gitkeep => .ansible/.lock | 0 .gitignore | 12 +- README.md | 35 - ansible/convention.yaml | 19 +- ansible/inventory/group_vars/all.yaml | 59 +- ansible/inventory/group_vars/hypervisor.yaml | 30 +- ansible/inventory/host_vars/app.yaml | 31 +- ansible/inventory/host_vars/auth.yaml | 11 +- ansible/inventory/host_vars/console.yaml | 16 +- ansible/inventory/host_vars/fw.yaml | 15 +- ansible/inventory/host_vars/infra.yaml | 15 +- ansible/inventory/hosts.ini | 2 +- ansible/playbooks/app/site.yaml | 21 +- ansible/playbooks/auth/site.yaml | 1 + ansible/playbooks/console/site.yaml | 46 +- ansible/playbooks/fw/site.yaml | 1 + ansible/playbooks/infra/site.yaml | 1 + ansible/playbooks/vmm/create_vm.yaml | 1 + ansible/playbooks/vmm/site.yaml | 10 + ansible/roles/app/handlers/main.yaml | 23 +- .../roles/app/tasks/node/set_data_volume.yaml | 41 + .../app/tasks/services/set_collabora.yaml | 2 +- .../roles/app/tasks/services/set_gitea.yaml | 6 +- .../roles/app/tasks/services/set_immich.yaml | 14 +- .../roles/app/tasks/services/set_mealie.yaml | 77 + .../roles/app/tasks/services/set_memos.yaml | 7 +- .../app/tasks/services/set_nextcloud.yaml | 24 +- .../roles/app/tasks/services/set_outline.yaml | 14 +- .../app/tasks/services/set_paperless.yaml | 14 +- .../roles/app/tasks/services/set_sure.yaml | 12 +- .../app/tasks/services/set_vaultwarden.yaml | 6 +- .../auth/tasks/services/set_authelia.yaml | 8 +- .../common/tasks/node/create_default_dir.yaml | 13 +- .../roles/common/tasks/node/set_networkd.yaml | 2 +- .../common/tasks/services/set_alloy.yaml | 15 +- .../common/tasks/services/set_caddy.yaml | 12 +- .../common/tasks/services/set_crowdsec.yaml | 4 - .../common/tasks/services/set_kopia.yaml | 4 +- .../common/tasks/services/set_podman.yaml | 6 +- .../console/tasks/node/create_vmm_init.yaml | 90 + .../console/tasks/node/load_secret_vars.yaml | 2 +- .../console/tasks/node/set_ssh_client.yaml | 8 +- .../console/tasks/services/set_cli_tools.yaml | 4 +- ansible/roles/fw/handlers/main.yaml | 2 +- ansible/roles/fw/tasks/services/set_bind.yaml | 2 +- ansible/roles/fw/tasks/services/set_ddns.yaml | 6 +- ansible/roles/fw/tasks/services/set_kea.yaml | 4 +- ansible/roles/infra/handlers/main.yaml | 2 +- .../infra/tasks/services/set_ca_server.yaml | 8 +- .../infra/tasks/services/set_grafana.yaml | 15 +- .../roles/infra/tasks/services/set_ldap.yaml | 11 +- .../roles/infra/tasks/services/set_loki.yaml | 8 +- .../infra/tasks/services/set_postgresql.yaml | 46 +- .../infra/tasks/services/set_prometheus.yaml | 8 +- .../tasks/services/set_x509-exporter.yaml | 8 +- ansible/roles/vmm/tasks/node/set_libvirt.yaml | 8 +- .../{app => vmm}/tasks/node/set_raid.yaml | 54 +- ansible/roles/vmm/tasks/vm/create_seed.yaml | 18 +- .../roles/vmm/tasks/vm/deploy_vm_init.yaml | 39 +- config/node/common/hosts.j2 | 3 + config/node/fw/nftables.conf.j2 | 4 +- config/node/vmm/networkd/00-vmm-eth0.link | 5 - config/node/vmm/networkd/00-vmm-eth0.link.j2 | 5 + config/node/vmm/networkd/01-vmm-eth1.link | 5 - config/node/vmm/networkd/01-vmm-eth1.link.j2 | 5 + ...10-vmm-br0.netdev => 10-vmm-br0.netdev.j2} | 0 ...11-vmm-br1.netdev => 11-vmm-br1.netdev.j2} | 0 ...mm-vlan1.netdev => 12-vmm-vlan1.netdev.j2} | 0 ...-vlan10.netdev => 13-vmm-vlan10.netdev.j2} | 0 ...-vlan20.netdev => 14-vmm-vlan20.netdev.j2} | 0 ...mm-eth0.network => 20-vmm-eth0.network.j2} | 0 ...mm-eth1.network => 21-vmm-eth1.network.j2} | 0 ...-vmm-br0.network => 22-vmm-br0.network.j2} | 0 ...-vmm-br1.network => 23-vmm-br1.network.j2} | 0 config/node/vmm/networkd/24-vmm-vlan1.network | 28 - .../node/vmm/networkd/24-vmm-vlan1.network.j2 | 28 + .../node/vmm/networkd/25-vmm-vlan10.network | 32 - .../vmm/networkd/25-vmm-vlan10.network.j2 | 32 + config/secrets/edit_secret.sh | 4 +- config/secrets/secrets.yaml | 94 +- .../containers/app/mealie/mealie.container.j2 | 49 + .../containers/app/nextcloud/ini/opcache.ini | 2 +- .../containers/app/nextcloud/ini/upload.ini | 2 +- .../app/outline/outline.container.j2 | 1 + .../auth/authelia/authelia.container.j2 | 4 +- .../auth/authelia/config/authelia.yaml.j2 | 71 +- .../common/caddy/etc/app/Caddyfile.j2 | 12 +- .../common/caddy/etc/auth/Caddyfile.j2 | 20 +- .../infra/postgresql/config/pg_hba.conf.j2 | 35 +- .../postgresql/config/postgresql.conf.j2 | 13 +- .../infra/postgresql/postgresql.container.j2 | 2 +- .../services/postgresql-data-backup@.service | 6 + .../systemd/app/btrfs/btrfs-scrub.service.j2 | 10 - .../systemd/common/alloy/config.alloy.j2 | 6 +- .../common/crowdsec/acquis.d/suricata.yaml | 2 +- .../common/crowdsec/etc/config.yaml.j2 | 2 +- .../common/kopia/kopia-backup.service.j2 | 11 +- .../systemd/common/kopia/kopia.env.j2 | 2 +- .../console/code-server/.vscode/settings.json | 28 + .../console/code-server/code-server.service | 14 + .../console/wsl-alive/wsl-alive.service | 10 + config/services/systemd/fw/ddns/ddns.sh | 34 +- .../systemd/vmm/btrfs/btrfs-scrub.service.j2 | 10 + .../{app => vmm}/btrfs/btrfs-scrub.timer.j2 | 2 +- .../systemd/vmm/libvirt/seeds/user-data.j2 | 2 +- .../vmm/libvirt/xml/storages/data-pool.xml | 8 + .../vmm/libvirt/xml/storages/images-pool.xml | 4 +- .../vmm/libvirt/xml/storages/seeds-pool.xml | 2 +- .../systemd/vmm/libvirt/xml/vms/vms.xml.j2 | 17 +- data/create_all_structure.sh | 56 - data/ilnmors_root_ca.crt | 11 - {docs/notes => data/images/seeds}/.gitkeep | 0 .../console/git.md => data/kopia/.gitkeep | 0 data/vmm_init/.gitkeep | 0 data/vmm_init/grub.d/iommu.cfg | 1 - data/vmm_init/modprobe.d/vfio.conf | 3 - data/vmm_init/network/00-vmm-eth0.link | 5 - data/vmm_init/network/01-vmm-eth1.link | 5 - data/vmm_init/network/10-vmm-br0.netdev | 3 - data/vmm_init/network/11-vmm-br1.netdev | 7 - data/vmm_init/network/12-vmm-vlan1.netdev | 6 - data/vmm_init/network/13-vmm-vlan10.netdev | 6 - data/vmm_init/network/14-vmm-vlan20.netdev | 6 - data/vmm_init/network/20-vmm-eth0.network | 6 - data/vmm_init/network/21-vmm-eth1.network | 15 - data/vmm_init/network/22-vmm-br0.network | 5 - data/vmm_init/network/23-vmm-br1.network | 17 - data/vmm_init/network/24-vmm-vlan1.network | 28 - data/vmm_init/network/25-vmm-vlan10.network | 32 - data/vmm_init/nftables.conf | 25 - data/vmm_init/ssh/local_ssh_ca.pub | 1 - .../ssh/sshd_config.d/prohibit_root.conf | 1 - data/vmm_init/ssh/sshd_config.d/ssh_ca.conf | 1 - data/vmm_init/sysctl.d/bridge.conf | 3 - docs/adr/001-architecture.md | 72 - docs/adr/002-network.md | 63 - docs/adr/003-pki.md | 57 - docs/adr/004-dns.md | 52 - docs/adr/005-ids-ips.md | 54 - docs/adr/006-secrets.md | 60 - docs/adr/007-backup.md | 77 - docs/adr/008-passthrough.md | 43 - docs/adr/009-isolation.md | 49 - docs/adr/010-provisioning.md | 35 - docs/adr/011-tls-communication.md | 33 - docs/adr/012-alerting.md | 45 - docs/archives/2025-06.tar.gz | Bin 0 -> 75524 bytes docs/archives/2025-06/on-premise.txt | 12151 ---------------- docs/archives/2025-06/홈 서버 구축 계획.txt | 102 - docs/archives/2025-12.tar.gz | Bin 0 -> 87711 bytes docs/archives/2025-12/01_plans/01_01_plans.md | 814 -- .../2025-12/01_plans/01_02_milestone.md | 167 - docs/archives/2025-12/02_theory/02_01_dns.md | 101 - docs/archives/2025-12/02_theory/02_02_dhcp.md | 55 - docs/archives/2025-12/02_theory/02_03_pki.md | 103 - docs/archives/2025-12/02_theory/02_04_tls.md | 92 - docs/archives/2025-12/02_theory/02_05_sso.md | 131 - .../archives/2025-12/02_theory/02_06_email.md | 242 - .../03_common/03_01_debian_configuration.md | 657 - .../2025-12/03_common/03_02_iptables.md | 273 - .../2025-12/03_common/03_03_podman.md | 306 - .../2025-12/03_common/03_04_crowdsec.md | 321 - .../archives/2025-12/03_common/03_05_redis.md | 107 - .../archives/2025-12/03_common/03_06_btrfs.md | 130 - .../2025-12/04_hypervisor/04_01_hypervisor.md | 277 - .../2025-12/05_firewall/05_01_opnsense_vm.md | 130 - .../05_firewall/05_02_opnsense_general.md | 91 - .../05_firewall/05_03_opnsense_interface.md | 159 - .../05_firewall/05_04_opnsense_rules.md | 79 - .../05_firewall/05_05_opnsense_suricata.md | 69 - .../05_firewall/05_06_opnsense_acme.md | 133 - .../2025-12/05_firewall/05_07_opnsense_kea.md | 40 - .../2025-12/06_network/06_01_net_vm.md | 107 - .../2025-12/06_network/06_02_net_ddns.md | 387 - .../2025-12/06_network/06_03_net_bind.md | 338 - .../06_network/06_04_net_adguard_home.md | 293 - .../2025-12/06_network/06_05_net_kea.md | 630 - .../2025-12/07_authorization/07_01_auth_vm.md | 148 - .../07_authorization/07_02_auth_step-ca.md | 320 - .../07_authorization/07_03_auth_main_caddy.md | 231 - .../07_authorization/07_04_auth_authentik.md | 315 - .../07_authorization/07_04_auth_lldap.md | 386 - .../07_authorization/07_05_auth_authelia.md | 651 - .../2025-12/08_development/08_01_dev_vm.md | 149 - .../08_development/08_02_dev_postgresql.md | 534 - .../08_development/08_03_dev_sidecar_caddy.md | 259 - .../08_development/08_04_dev_code-server.md | 341 - .../2025-12/09_application/09_01_app_vm.md | 253 - docs/archives/2025-12/console.md | 360 - docs/archives/2025-12/scripts.md | 452 - docs/archives/2026-03.tar.gz | Bin 0 -> 81581 bytes docs/archives/2026-06.tar.gz | Bin 0 -> 99371 bytes .../services}/app/affine/affine.container.j2 | 0 .../services/app/affine}/affine.md | 0 .../services/app/affine/authelia.yaml | 25 + .../services/app/affine/group_vars.yaml | 16 + .../app/affine}/manticore.container.j2 | 0 .../services/app/affine/secret.example.yaml | 13 + .../services/app/affine}/set_affine.yaml | 18 +- .../services/console/script/extract_secret.sh | 4 +- docs/issues/affine/260420_android_oidc.md | 33 - docs/issues/crowdsec/260321_actual_budget.md | 34 - docs/issues/crowdsec/260321_immich.md | 33 - docs/issues/crowdsec/260404_opencloud.md | 33 - docs/issues/crowdsec/260502_nextcloud.md | 45 - docs/issues/crowdsec/260516_outline.md | 26 - docs/runbook/00-operate.md | 108 - docs/runbook/01-windows.md | 260 - docs/runbook/02-certificates.md | 169 - docs/runbook/03-wireguard.md | 23 - docs/runbook/04-hypervisor.md | 162 - docs/runbook/05-hardwares.md | 220 - docs/runbook/06-kopia.md | 248 - docs/runbook/07-git.md | 121 - docs/services/app/collabora.md | 28 - docs/services/app/gitea.md | 93 - docs/services/app/igpu_firmware.md | 12 - docs/services/app/immich.md | 86 - docs/services/app/memos.md | 74 - docs/services/app/nextcloud.md | 106 - docs/services/app/outline.md | 52 - docs/services/app/paperless-ngx.md | 121 - docs/services/app/sure.md | 67 - docs/services/app/vaultwarden.md | 49 - docs/services/common/alloy.md | 35 - docs/services/common/caddy.md | 51 - docs/services/common/crowdsec.md | 250 - docs/services/common/kopia.md | 14 - docs/services/fw/kea.md | 29 - docs/services/infra/ca.md | 146 - docs/services/infra/grafana.md | 20 - docs/services/infra/ldap.md | 154 - docs/services/infra/loki.md | 12 - docs/services/infra/postgresql.md | 100 - docs/services/infra/prometheus.md | 12 - docs/services/systemd/systemd-networkd.md | 35 - docs/services/systemd/systemd-quadlet.md | 67 - docs/services/vmm/libvirt/cloud-init.md | 125 - docs/services/vmm/libvirt/undefine.md | 18 - docs/specifications/environments.md | 168 - docs/specifications/hardwares.md | 67 - docs/specifications/matrix.md | 117 - docs/theories/network/dhcp.md | 53 - docs/theories/network/dns.md | 99 - docs/theories/network/email.md | 241 - docs/theories/network/link-local.md | 30 - docs/theories/network/tls.md | 90 - docs/theories/pki/pki.md | 102 - docs/theories/pki/sso.md | 129 - docs/theories/virtualization/passthrough.md | 15 - 250 files changed, 1074 insertions(+), 28750 deletions(-) rename data/volumes/.gitkeep => .ansible/.lock (100%) delete mode 100644 README.md create mode 100644 ansible/roles/app/tasks/node/set_data_volume.yaml create mode 100644 ansible/roles/app/tasks/services/set_mealie.yaml create mode 100644 ansible/roles/console/tasks/node/create_vmm_init.yaml rename ansible/roles/{app => vmm}/tasks/node/set_raid.yaml (64%) delete mode 100644 config/node/vmm/networkd/00-vmm-eth0.link create mode 100644 config/node/vmm/networkd/00-vmm-eth0.link.j2 delete mode 100644 config/node/vmm/networkd/01-vmm-eth1.link create mode 100644 config/node/vmm/networkd/01-vmm-eth1.link.j2 rename config/node/vmm/networkd/{10-vmm-br0.netdev => 10-vmm-br0.netdev.j2} (100%) rename config/node/vmm/networkd/{11-vmm-br1.netdev => 11-vmm-br1.netdev.j2} (100%) rename config/node/vmm/networkd/{12-vmm-vlan1.netdev => 12-vmm-vlan1.netdev.j2} (100%) rename config/node/vmm/networkd/{13-vmm-vlan10.netdev => 13-vmm-vlan10.netdev.j2} (100%) rename config/node/vmm/networkd/{14-vmm-vlan20.netdev => 14-vmm-vlan20.netdev.j2} (100%) rename config/node/vmm/networkd/{20-vmm-eth0.network => 20-vmm-eth0.network.j2} (100%) rename config/node/vmm/networkd/{21-vmm-eth1.network => 21-vmm-eth1.network.j2} (100%) rename config/node/vmm/networkd/{22-vmm-br0.network => 22-vmm-br0.network.j2} (100%) rename config/node/vmm/networkd/{23-vmm-br1.network => 23-vmm-br1.network.j2} (100%) delete mode 100644 config/node/vmm/networkd/24-vmm-vlan1.network create mode 100644 config/node/vmm/networkd/24-vmm-vlan1.network.j2 delete mode 100644 config/node/vmm/networkd/25-vmm-vlan10.network create mode 100644 config/node/vmm/networkd/25-vmm-vlan10.network.j2 create mode 100644 config/services/containers/app/mealie/mealie.container.j2 delete mode 100644 config/services/systemd/app/btrfs/btrfs-scrub.service.j2 create mode 100644 config/services/systemd/console/code-server/.vscode/settings.json create mode 100644 config/services/systemd/console/code-server/code-server.service create mode 100644 config/services/systemd/console/wsl-alive/wsl-alive.service create mode 100644 config/services/systemd/vmm/btrfs/btrfs-scrub.service.j2 rename config/services/systemd/{app => vmm}/btrfs/btrfs-scrub.timer.j2 (75%) create mode 100644 config/services/systemd/vmm/libvirt/xml/storages/data-pool.xml delete mode 100755 data/create_all_structure.sh delete mode 100644 data/ilnmors_root_ca.crt rename {docs/notes => data/images/seeds}/.gitkeep (100%) rename docs/services/console/git.md => data/kopia/.gitkeep (100%) create mode 100644 data/vmm_init/.gitkeep delete mode 100644 data/vmm_init/grub.d/iommu.cfg delete mode 100644 data/vmm_init/modprobe.d/vfio.conf delete mode 100644 data/vmm_init/network/00-vmm-eth0.link delete mode 100644 data/vmm_init/network/01-vmm-eth1.link delete mode 100644 data/vmm_init/network/10-vmm-br0.netdev delete mode 100644 data/vmm_init/network/11-vmm-br1.netdev delete mode 100644 data/vmm_init/network/12-vmm-vlan1.netdev delete mode 100644 data/vmm_init/network/13-vmm-vlan10.netdev delete mode 100644 data/vmm_init/network/14-vmm-vlan20.netdev delete mode 100644 data/vmm_init/network/20-vmm-eth0.network delete mode 100644 data/vmm_init/network/21-vmm-eth1.network delete mode 100644 data/vmm_init/network/22-vmm-br0.network delete mode 100644 data/vmm_init/network/23-vmm-br1.network delete mode 100644 data/vmm_init/network/24-vmm-vlan1.network delete mode 100644 data/vmm_init/network/25-vmm-vlan10.network delete mode 100644 data/vmm_init/nftables.conf delete mode 100644 data/vmm_init/ssh/local_ssh_ca.pub delete mode 100644 data/vmm_init/ssh/sshd_config.d/prohibit_root.conf delete mode 100644 data/vmm_init/ssh/sshd_config.d/ssh_ca.conf delete mode 100644 data/vmm_init/sysctl.d/bridge.conf delete mode 100644 docs/adr/001-architecture.md delete mode 100644 docs/adr/002-network.md delete mode 100644 docs/adr/003-pki.md delete mode 100644 docs/adr/004-dns.md delete mode 100644 docs/adr/005-ids-ips.md delete mode 100644 docs/adr/006-secrets.md delete mode 100644 docs/adr/007-backup.md delete mode 100644 docs/adr/008-passthrough.md delete mode 100644 docs/adr/009-isolation.md delete mode 100644 docs/adr/010-provisioning.md delete mode 100644 docs/adr/011-tls-communication.md delete mode 100644 docs/adr/012-alerting.md create mode 100644 docs/archives/2025-06.tar.gz delete mode 100644 docs/archives/2025-06/on-premise.txt delete mode 100644 docs/archives/2025-06/홈 서버 구축 계획.txt create mode 100644 docs/archives/2025-12.tar.gz delete mode 100644 docs/archives/2025-12/01_plans/01_01_plans.md delete mode 100644 docs/archives/2025-12/01_plans/01_02_milestone.md delete mode 100644 docs/archives/2025-12/02_theory/02_01_dns.md delete mode 100644 docs/archives/2025-12/02_theory/02_02_dhcp.md delete mode 100644 docs/archives/2025-12/02_theory/02_03_pki.md delete mode 100644 docs/archives/2025-12/02_theory/02_04_tls.md delete mode 100644 docs/archives/2025-12/02_theory/02_05_sso.md delete mode 100644 docs/archives/2025-12/02_theory/02_06_email.md delete mode 100644 docs/archives/2025-12/03_common/03_01_debian_configuration.md delete mode 100644 docs/archives/2025-12/03_common/03_02_iptables.md delete mode 100644 docs/archives/2025-12/03_common/03_03_podman.md delete mode 100644 docs/archives/2025-12/03_common/03_04_crowdsec.md delete mode 100644 docs/archives/2025-12/03_common/03_05_redis.md delete mode 100644 docs/archives/2025-12/03_common/03_06_btrfs.md delete mode 100644 docs/archives/2025-12/04_hypervisor/04_01_hypervisor.md delete mode 100644 docs/archives/2025-12/05_firewall/05_01_opnsense_vm.md delete mode 100644 docs/archives/2025-12/05_firewall/05_02_opnsense_general.md delete mode 100644 docs/archives/2025-12/05_firewall/05_03_opnsense_interface.md delete mode 100644 docs/archives/2025-12/05_firewall/05_04_opnsense_rules.md delete mode 100644 docs/archives/2025-12/05_firewall/05_05_opnsense_suricata.md delete mode 100644 docs/archives/2025-12/05_firewall/05_06_opnsense_acme.md delete mode 100644 docs/archives/2025-12/05_firewall/05_07_opnsense_kea.md delete mode 100644 docs/archives/2025-12/06_network/06_01_net_vm.md delete mode 100644 docs/archives/2025-12/06_network/06_02_net_ddns.md delete mode 100644 docs/archives/2025-12/06_network/06_03_net_bind.md delete mode 100644 docs/archives/2025-12/06_network/06_04_net_adguard_home.md delete mode 100644 docs/archives/2025-12/06_network/06_05_net_kea.md delete mode 100644 docs/archives/2025-12/07_authorization/07_01_auth_vm.md delete mode 100644 docs/archives/2025-12/07_authorization/07_02_auth_step-ca.md delete mode 100644 docs/archives/2025-12/07_authorization/07_03_auth_main_caddy.md delete mode 100644 docs/archives/2025-12/07_authorization/07_04_auth_authentik.md delete mode 100644 docs/archives/2025-12/07_authorization/07_04_auth_lldap.md delete mode 100644 docs/archives/2025-12/07_authorization/07_05_auth_authelia.md delete mode 100644 docs/archives/2025-12/08_development/08_01_dev_vm.md delete mode 100644 docs/archives/2025-12/08_development/08_02_dev_postgresql.md delete mode 100644 docs/archives/2025-12/08_development/08_03_dev_sidecar_caddy.md delete mode 100644 docs/archives/2025-12/08_development/08_04_dev_code-server.md delete mode 100644 docs/archives/2025-12/09_application/09_01_app_vm.md delete mode 100644 docs/archives/2025-12/console.md delete mode 100644 docs/archives/2025-12/scripts.md create mode 100644 docs/archives/2026-03.tar.gz create mode 100644 docs/archives/2026-06.tar.gz rename {config/services/containers => docs/archives/services}/app/affine/affine.container.j2 (100%) rename docs/{services/app => archives/services/app/affine}/affine.md (100%) create mode 100644 docs/archives/services/app/affine/authelia.yaml create mode 100644 docs/archives/services/app/affine/group_vars.yaml rename {config/services/containers/app/manticore => docs/archives/services/app/affine}/manticore.container.j2 (100%) create mode 100644 docs/archives/services/app/affine/secret.example.yaml rename {ansible/roles/app/tasks/services => docs/archives/services/app/affine}/set_affine.yaml (95%) delete mode 100644 docs/issues/affine/260420_android_oidc.md delete mode 100644 docs/issues/crowdsec/260321_actual_budget.md delete mode 100644 docs/issues/crowdsec/260321_immich.md delete mode 100644 docs/issues/crowdsec/260404_opencloud.md delete mode 100644 docs/issues/crowdsec/260502_nextcloud.md delete mode 100644 docs/issues/crowdsec/260516_outline.md delete mode 100644 docs/runbook/00-operate.md delete mode 100644 docs/runbook/01-windows.md delete mode 100644 docs/runbook/02-certificates.md delete mode 100644 docs/runbook/03-wireguard.md delete mode 100644 docs/runbook/04-hypervisor.md delete mode 100644 docs/runbook/05-hardwares.md delete mode 100644 docs/runbook/06-kopia.md delete mode 100644 docs/runbook/07-git.md delete mode 100644 docs/services/app/collabora.md delete mode 100644 docs/services/app/gitea.md delete mode 100644 docs/services/app/igpu_firmware.md delete mode 100644 docs/services/app/immich.md delete mode 100644 docs/services/app/memos.md delete mode 100644 docs/services/app/nextcloud.md delete mode 100644 docs/services/app/outline.md delete mode 100644 docs/services/app/paperless-ngx.md delete mode 100644 docs/services/app/sure.md delete mode 100644 docs/services/app/vaultwarden.md delete mode 100644 docs/services/common/alloy.md delete mode 100644 docs/services/common/caddy.md delete mode 100644 docs/services/common/crowdsec.md delete mode 100644 docs/services/common/kopia.md delete mode 100644 docs/services/fw/kea.md delete mode 100644 docs/services/infra/ca.md delete mode 100644 docs/services/infra/grafana.md delete mode 100644 docs/services/infra/ldap.md delete mode 100644 docs/services/infra/loki.md delete mode 100644 docs/services/infra/postgresql.md delete mode 100644 docs/services/infra/prometheus.md delete mode 100644 docs/services/systemd/systemd-networkd.md delete mode 100644 docs/services/systemd/systemd-quadlet.md delete mode 100644 docs/services/vmm/libvirt/cloud-init.md delete mode 100644 docs/services/vmm/libvirt/undefine.md delete mode 100644 docs/specifications/environments.md delete mode 100644 docs/specifications/hardwares.md delete mode 100644 docs/specifications/matrix.md delete mode 100644 docs/theories/network/dhcp.md delete mode 100644 docs/theories/network/dns.md delete mode 100644 docs/theories/network/email.md delete mode 100644 docs/theories/network/link-local.md delete mode 100644 docs/theories/network/tls.md delete mode 100644 docs/theories/pki/pki.md delete mode 100644 docs/theories/pki/sso.md delete mode 100644 docs/theories/virtualization/passthrough.md diff --git a/data/volumes/.gitkeep b/.ansible/.lock similarity index 100% rename from data/volumes/.gitkeep rename to .ansible/.lock diff --git a/.gitignore b/.gitignore index cd272f2..05b9b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ -.ansible data/bin/* -data/volumes/* -data/images/* -!data/images/containers data/images/containers/* -docs/archives/textfiles/ -docs/notes/* +data/images/seeds/* +data/vmm_init/* +*.raw +*.tar +*.qcow2 *.sql +*.iso !.gitkeep diff --git a/README.md b/README.md deleted file mode 100644 index 1f95d24..0000000 --- a/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# ilnmors homelab README - -This homelab project implements single-node On-premise IaaS system. The homelab contains virtual machines which are divided by their roles, such as private firewall, DNS, PKI, LDAP and database, SSO(OIDC). The standard domain is used to implement this system without specific vendors. All components are defined as code and initiated by IaC (Ansible) except hypervisor initial configuration. - -## RTO and RPO - -### RPO -- Each backup guarantees 24 hours RPO - - DB dumps are backed up at 12:00 AM - - Stateful data in app vm is backed up at 03:00 AM -- The maximum inconsistency window between DB dumps and stateful data can be 27 hours. - - The different backup time. - -### RTO -- Feb/25/2026 - Reprovisioning Hypervisor and vms - - RTO: 1 hour 30 min - verified - - Manual install and set vmm: 20 min - - Create and reprovision fw including services: 15 min - - Create and reprovision infra including services: 20 min - - Create and reprovision auth including services: 10 min - - Create and reprovision app except services: 10 min - - Intermediate tasks (ACME issuance, DNS propagation, etc.): 15 min - -- Mar/5/2026 - Reprovisioning Hardware and Hypervisor and vms - - RTO: 2 hour 20 min - - console: 15min - verified - - certificate: 0 min (When it needs to be created, RTO will be 20 min) - not verified - - wireguard: 0 min (When it needs to be created, RTO will be 1 min) - not verified - - hypervisor(+fw): 45 min - verified - - switch: 1 min - verified - - dsm: 30 min - verified - - kopia: 0 min (When it needs to be created, RTO will be 10 min) - verified - - Extra vms: 30 min - verified - - Etc: 30 min - diff --git a/ansible/convention.yaml b/ansible/convention.yaml index 5fac38d..27b6c85 100644 --- a/ansible/convention.yaml +++ b/ansible/convention.yaml @@ -1,27 +1,28 @@ --- -# `""` should be used for every string values -# except name, boolean, varible(environment) name, number (only for decimal) +# String values need `""` +# Task names, boolean values, environment names, decimal numbers don't need `""` - name: Convention task delegate_to: "host" delegate_facts: true environment: - env: "environment" + env: "value" ansible.builtin.file: - # Identifier: name:, src, dest, url - path: "/path/of/example" - # State + # Identifier: name, src, dest, url, etc. + path: "/example/path" + # state parameters state: "directory" owner: "root" group: "root" mode: "0640" - # optional data + # optional parameters recurse: true loop: - "list" loop_control: label: "{{ item }}" become: true - # become_user: "root" + # default become user is root + become_user: "user" when: condition.stat.exists changed_when: condition.rc != 0 failed_when: condition.rc == 0 @@ -30,6 +31,8 @@ listen: "notification_example" no_log: true run_once: true + # Ensure execution even if it fails (it prints errors on the screen, but it ignores them) + ignore_errors: true tags: - "always" - "init" diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index 9a4ea4e..4c980b8 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -1,15 +1,14 @@ --- # Global vars -ansible_ssh_private_key_file: "/etc/secrets/{{ hostvars['console']['node']['uid'] }}/id_console" timezone: "Asia/Seoul" +ansible_ssh_private_key_file: "/etc/secrets/{{ hostvars['console']['node']['uid'] }}/id_console" # CA root_cert_filename: "ilnmors_root_ca.crt" intermediate_cert_filename: "ilnmors_intermediate_ca.crt" intermediate_key_filename: "ilnmors_intermediate_ca.key" - -# local SAN and SSH SAN should be updated manually on host_vars +# Domains domain: public: "ilnmors.com" internal: "ilnmors.internal" @@ -67,7 +66,7 @@ services: grafana: domain: "grafana" ports: - http: "3000" # Infra server: Internal ports + http: "3000" subuid: "100471" caddy: ports: @@ -98,7 +97,7 @@ services: public: "gitea" internal: "gitea.app" ports: - http: "3000" # App server: Public ports + http: "3001" subuid: "100999" immich: domain: @@ -118,16 +117,6 @@ services: http: "8001" redis: "6380" subuid: "100999" - manticore: - subuid: "100998" - affine: - domain: - public: "affine" - internal: "affine.app" - ports: - http: "3010" - redis: "6381" - manticore: "9308" nextcloud: domain: public: "nextcloud" @@ -148,7 +137,7 @@ services: public: "sure" internal: "sure.app" ports: - http: "3001" + http: "3002" redis: "6383" subuid: "100999" outline: @@ -156,7 +145,7 @@ services: public: "outline" internal: "outline.app" ports: - http: "3002" + http: "3004" redis: "6384" subuid: "101000" memos: @@ -164,43 +153,49 @@ services: public: "memos" internal: "memos.app" ports: - http: "3004" + http: "3005" subuid: "110000" + mealie: + domain: + public: "mealie" + internal: "mealie.app" + ports: + http: "9001" + subuid: "100910" version: packages: - sops: "3.12.1" + sops: "3.13.1" step: "0.30.2" - kopia: "0.22.3" + kopia: "0.23.0" blocky: "0.29.0" alloy: "1.16.1" containers: # common - caddy: "2.11.2" + caddy: "2.11.4" # infra step: "0.30.2" ldap: "v0.6.3" x509-exporter: "4.1.0" - prometheus: "v3.11.3" + prometheus: "v3.12.0" loki: "3.7.1" grafana: "13.0.1" ## Postgresql - postgresql: "18.3" + postgresql: "18.4" # For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml # pgvector: "v0.8.1" vectorchord: "1.1.1" # Auth - authelia: "4.39.19" + authelia: "4.39.20" # App vaultwarden: "1.36.0" gitea: "1.26.1" - redis: "8.6.3" + redis: "8.8.0" immich: "v2.7.5" paperless: "2.20.15" - manticore: "25.0.0" - affine: "0.26.3" - nextcloud: "33.0.3" - collabora: "25.04.9.4.1" - sure: "0.7.0-hotfix.2" - outline: "1.7.1" - memos: "0.28.0" + nextcloud: "33.0.5" + collabora: "25.04.10.3.1" + sure: "0.7.1" + outline: "1.8.1" + memos: "0.29.1" + mealie: "v3.19.2" diff --git a/ansible/inventory/group_vars/hypervisor.yaml b/ansible/inventory/group_vars/hypervisor.yaml index 27793fe..3cafb42 100644 --- a/ansible/inventory/group_vars/hypervisor.yaml +++ b/ansible/inventory/group_vars/hypervisor.yaml @@ -1,7 +1,33 @@ --- node: name: "vmm" + group: "svadmins" uid: 2000 home_path: "/home/vmm" - ssh_san: "vmm,vmm_init,vmm.ilnmors.internal,init.vmm.ilnmors.internal" - local_san: "localhost vmm.ilnmors.internal" + libvirt_path: "/var/lib/libvirt" + ssh_san: "{{ node['name'] }},{{ node['name'] }}_init,{{ node['name'] }}.{{ domain['internal'] }},init.{{ node['name'] }}.{{ domain['internal'] }}" + local_san: "localhost {{ node['name'] }}.{{ domain['internal'] }}" + +hardware: + mac: + nic0: "c8:ff:bf:07:02:34" + nic1: "c8:ff:bf:07:02:33" + sfpp0: "c8:ff:bf:07:02:31" + sfpp1: "c8:ff:bf:07:02:32" + igpu: + id: "8086:4628" + address: "0000:00:02.0" + domain: "0x0000" + bus: "0x00" + slot: "0x02" + function: "0x0" + hdd: + data: + hdd0: "WMC6N0E617S8" + hdd1: "P6HVTL0W" + hdd2: "P2GH354H" + hdd3: "P6GJKSMP" + btrfs: + label: "DATA" + level: "raid10" + mount_point: "{{ node['libvirt_path'] }}/data" diff --git a/ansible/inventory/host_vars/app.yaml b/ansible/inventory/host_vars/app.yaml index 13e4f51..fd5e841 100644 --- a/ansible/inventory/host_vars/app.yaml +++ b/ansible/inventory/host_vars/app.yaml @@ -2,17 +2,29 @@ # Node Factors node: name: "app" + group: "svadmins" uid: 2004 home_path: "/home/app" - ssh_san: "app,app.ilnmors.internal" - local_san: "localhost app.ilnmors.internal" + ssh_san: "{{ node['name'] }},{{ node['name'] }}.{{ domain['internal'] }}" + local_san: "localhost {{ node['name'] }}.{{ domain['internal'] }}" # VM Factors vm: name: "app" - cpu: 4 + cpu: 6 + cpu_pin: | + + + + + + + shares: 1024 memory: 16 storage: 256 + data_storage: + name: "vdb" + size: 3.5 lan_mac: "0a:49:6e:4d:03:00" lan_net: "lan-net" lan_group: "vlan10-access" @@ -26,16 +38,3 @@ vm: bus: "0x00" slot: "0x02" function: "0x0" - sata_controller: # Additional SATA Controller - address: "0000:04:00.0" - domain: "0x0000" - bus: "0x04" - slot: "0x00" - function: "0x0" - -# BTRFS configuration for hdd which is passthroughed -storage: - btrfs: - label: "APP_DATA" - level: "raid10" - mount_point: "/home/app/data" diff --git a/ansible/inventory/host_vars/auth.yaml b/ansible/inventory/host_vars/auth.yaml index aee9496..bbf70f6 100644 --- a/ansible/inventory/host_vars/auth.yaml +++ b/ansible/inventory/host_vars/auth.yaml @@ -2,15 +2,20 @@ # Node Factors node: name: "auth" + group: "svadmins" uid: 2003 home_path: "/home/auth" - ssh_san: "auth,auth.ilnmors.internal" - local_san: "localhost auth.ilnmors.internal" + ssh_san: "{{ node['name'] }},{{ node['name'] }}.{{ domain['internal'] }}" + local_san: "localhost {{ node['name'] }}.{{ domain['internal'] }}" # VM Factors vm: name: "auth" cpu: 2 - shares: 512 + cpu_pin: | + + + + shares: 1024 memory: 2 storage: 64 lan_mac: "0a:49:6e:4d:02:00" diff --git a/ansible/inventory/host_vars/console.yaml b/ansible/inventory/host_vars/console.yaml index 0bfc7f6..f94b01e 100644 --- a/ansible/inventory/host_vars/console.yaml +++ b/ansible/inventory/host_vars/console.yaml @@ -10,17 +10,17 @@ age_key: !vault | 33653638373537323363316363646534323362353836373665636265663939353862663532313230 30643038313138626464626161373835646665326334393834616234643931656536343130316238 61656264643830616639 -# Node Factors + node: name: "console" + group: "svadmins" uid: 2999 home_path: "/home/console" - workspace_path: "{{ node.home_path }}/workspace" - homelab_path: "{{ node.home_path }}/workspace/homelab" - data_path: "{{ node.homelab_path }}/data" - config_path: "{{ node.homelab_path }}/config" - ssh_san: "console,console.ilnmors.internal" + workspace_path: "{{ node['home_path'] }}/workspace" + homelab_path: "{{ node['home_path'] }}/workspace/homelab" + data_path: "{{ node['homelab_path'] }}/data" + config_path: "{{ node['homelab_path'] }}/config" + ssh_san: "{{ node['name'] }},{{ node['name'] }}.{{ domain['internal'] }}" ssh_users: "vmm,fw,infra,auth,app" # add the hostname of wsl, it is needed to improve the sudo problem - local_san: "localhost console.ilnmors.internal surface" -# ansible_python_interpreter: "{{ ansible_playbook_python }}" + local_san: "localhost {{ node['name'] }} {{ node['name'] }}.{{ domain['internal'] }}" diff --git a/ansible/inventory/host_vars/fw.yaml b/ansible/inventory/host_vars/fw.yaml index af0bdeb..690c7f6 100644 --- a/ansible/inventory/host_vars/fw.yaml +++ b/ansible/inventory/host_vars/fw.yaml @@ -2,15 +2,22 @@ # Node Factors node: name: "fw" + group: "svadmins" uid: 2001 home_path: "/home/fw" - ssh_san: "fw,fw.ilnmors.internal" - local_san: "localhost fw.ilnmors.internal" + ssh_san: "{{ node['name'] }},{{ node['name'] }}.{{ domain['internal'] }}" + local_san: "localhost {{ node['name'] }}.{{ domain['internal'] }}" # VM Factors vm: name: "fw" - cpu: 2 + cpu: 4 + cpu_pin: | + + + + + shares: 2048 memory: 4 storage: 64 @@ -21,7 +28,7 @@ vm: lan_group: "vlan-trunk" # Network Factors -# LLA is like MAC address for L3 (Network layer). Usually, subnet is used to seperate network. +# LLA is like MAC address for L3 (Network layer). Usually, subnet is used to separate network. network4: subnet: client: "192.168.1.0/24" diff --git a/ansible/inventory/host_vars/infra.yaml b/ansible/inventory/host_vars/infra.yaml index e7243e2..2d9961a 100644 --- a/ansible/inventory/host_vars/infra.yaml +++ b/ansible/inventory/host_vars/infra.yaml @@ -2,16 +2,23 @@ # Node Factors node: name: "infra" + group: "svadmins" uid: 2002 home_path: "/home/infra" - ssh_san: "infra,infra.ilnmors.internal" - local_san: "localhost infra.ilnmors.internal" + ssh_san: "{{ node['name'] }},{{ node['name'] }}.{{ domain['internal'] }}" + local_san: "localhost {{ node['name'] }}.{{ domain['internal'] }}" # VM Factors vm: name: "infra" - cpu: 2 - shares: 1024 + cpu: 4 + cpu_pin: | + + + + + + shares: 2048 memory: 6 storage: 256 lan_mac: "0a:49:6e:4d:01:00" diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini index 227ae78..8fcf9fe 100644 --- a/ansible/inventory/hosts.ini +++ b/ansible/inventory/hosts.ini @@ -11,4 +11,4 @@ vmm ansible_host=vmm.ilnmors.internal ansible_user=vmm fw ansible_host=fw.ilnmors.internal ansible_user=fw infra ansible_host=infra.ilnmors.internal ansible_user=infra auth ansible_host=auth.ilnmors.internal ansible_user=auth -app ansible_host=app.ilnmors.internal ansible_user=app \ No newline at end of file +app ansible_host=app.ilnmors.internal ansible_user=app diff --git a/ansible/playbooks/app/site.yaml b/ansible/playbooks/app/site.yaml index 367ac95..6526248 100644 --- a/ansible/playbooks/app/site.yaml +++ b/ansible/playbooks/app/site.yaml @@ -20,6 +20,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['app'] }}" + no_log: true tags: ["always"] tasks: @@ -97,10 +98,10 @@ become: true tags: ["init", "install-packages"] - - name: Set raid + - name: Set data volume ansible.builtin.include_role: name: "app" - tasks_from: "node/set_raid" + tasks_from: "node/set_data_volume" apply: tags: ["init", "raid"] tags: ["init", "raid"] @@ -193,14 +194,6 @@ tags: ["site", "paperless"] tags: ["site", "paperless"] - - name: Set affine - ansible.builtin.include_role: - name: "app" - tasks_from: "services/set_affine" - apply: - tags: ["site", "affine"] - tags: ["site", "affine"] - - name: Set nextcloud ansible.builtin.include_role: name: "app" @@ -241,6 +234,14 @@ tags: ["site", "memos"] tags: ["site", "memos"] + - name: Set mealie + ansible.builtin.include_role: + name: "app" + tasks_from: "services/set_mealie" + apply: + tags: ["site", "mealie"] + tags: ["site", "mealie"] + - name: Flush handlers right now ansible.builtin.meta: "flush_handlers" diff --git a/ansible/playbooks/auth/site.yaml b/ansible/playbooks/auth/site.yaml index 31ba638..fb56629 100644 --- a/ansible/playbooks/auth/site.yaml +++ b/ansible/playbooks/auth/site.yaml @@ -20,6 +20,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['auth'] }}" + no_log: true tags: ["always"] tasks: diff --git a/ansible/playbooks/console/site.yaml b/ansible/playbooks/console/site.yaml index 1f6d48c..a79afac 100644 --- a/ansible/playbooks/console/site.yaml +++ b/ansible/playbooks/console/site.yaml @@ -20,6 +20,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['console'] }}" + no_log: true tags: ["always"] tasks: @@ -70,32 +71,6 @@ tags: ["init", "ssh_client"] tags: ["init", "ssh_client"] - - name: Check file permissions - ansible.builtin.file: - path: "{{ node['workspace_path'] }}/{{ item }}" - state: "directory" - owner: "{{ ansible_user }}" - group: "svadmins" - mode: "u=rwX,g=,o=" - recurse: true - loop: - - "homelab/ansible" - - "homelab/config" - - "homelab/docs" - - "university" - tags: ["init", "site", "file_permission"] - # kopia snashot is mounted on homelab/data/volumes. - # NEVER CHANGE permission and owners - - - name: Download vm cloud-init - ansible.builtin.get_url: - url: "https://cdimage.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2" - dest: "{{ node['data_path'] }}/images/debian-13-generic-amd64.qcow2" - owner: "console" - group: "svadmins" - mode: "0600" - tags: ["init", "site", "cloud-init-image"] - - name: Install packages ansible.builtin.apt: name: @@ -108,8 +83,6 @@ - "logrotate" - "nftables" - "build-essential" - - "g++" - - "gcc" - "fuse3" state: "present" become: true @@ -123,6 +96,23 @@ tags: ["init", "site", "tools"] tags: ["init", "site", "tools"] + - name: Set vmm_init config + ansible.builtin.include_role: + name: "console" + tasks_from: "node/create_vmm_init" + apply: + tags: ["site", "vmm_init"] + tags: ["site", "vmm_init"] + + - name: Download vm cloud-init + ansible.builtin.get_url: + url: "https://cdimage.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2" + dest: "{{ node['data_path'] }}/images/debian-13-generic-amd64.qcow2" + owner: "console" + group: "svadmins" + mode: "0600" + tags: ["init", "site", "cloud-init-image"] + - name: Set kopia ansible.builtin.include_role: name: "common" diff --git a/ansible/playbooks/fw/site.yaml b/ansible/playbooks/fw/site.yaml index d511bc6..6aa8f68 100644 --- a/ansible/playbooks/fw/site.yaml +++ b/ansible/playbooks/fw/site.yaml @@ -20,6 +20,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['fw'] }}" + no_log: true tags: ["always"] tasks: diff --git a/ansible/playbooks/infra/site.yaml b/ansible/playbooks/infra/site.yaml index 6184fad..a0bd5b7 100644 --- a/ansible/playbooks/infra/site.yaml +++ b/ansible/playbooks/infra/site.yaml @@ -20,6 +20,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['infra'] }}" + no_log: true tags: ["always"] tasks: diff --git a/ansible/playbooks/vmm/create_vm.yaml b/ansible/playbooks/vmm/create_vm.yaml index 28695eb..4b01eae 100644 --- a/ansible/playbooks/vmm/create_vm.yaml +++ b/ansible/playbooks/vmm/create_vm.yaml @@ -36,6 +36,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['vmm'] }}" + no_log: true - name: Create seed file ansible.builtin.include_role: diff --git a/ansible/playbooks/vmm/site.yaml b/ansible/playbooks/vmm/site.yaml index 1cfb755..ee28469 100644 --- a/ansible/playbooks/vmm/site.yaml +++ b/ansible/playbooks/vmm/site.yaml @@ -27,6 +27,7 @@ - name: Set become password ansible.builtin.set_fact: ansible_become_pass: "{{ hostvars['console']['sudo']['password']['vmm'] }}" + no_log: true tags: ["always"] tasks: # init @@ -100,6 +101,14 @@ tags: ["init", "linger"] tags: ["init", "linger"] + - name: Set raid + ansible.builtin.include_role: + name: "vmm" + tasks_from: "node/set_raid" + apply: + tags: ["init", "raid"] + tags: ["init", "raid"] + - name: Set libvirt ansible.builtin.include_role: name: "vmm" @@ -155,6 +164,7 @@ - ksmtuned - libvirt-daemon-system - virt-top + - btrfs-progs - python3 - python3-apt - python3-libvirt diff --git a/ansible/roles/app/handlers/main.yaml b/ansible/roles/app/handlers/main.yaml index d492b91..f7e9736 100644 --- a/ansible/roles/app/handlers/main.yaml +++ b/ansible/roles/app/handlers/main.yaml @@ -54,18 +54,6 @@ listen: "notification_restart_paperless" ignore_errors: true # noqa: ignore-errors -- name: Restart affine - ansible.builtin.systemd: - name: "affine.service" - state: "restarted" - enabled: true - daemon_reload: true - scope: "user" - when: is_affine_init.stat.exists - changed_when: false - listen: "notification_restart_affine" - ignore_errors: true # noqa: ignore-errors - - name: Restart nextcloud ansible.builtin.systemd: name: "nextcloud.service" @@ -125,3 +113,14 @@ changed_when: false listen: "notification_restart_memos" ignore_errors: true # noqa: ignore-errors + +- name: Restart mealie + ansible.builtin.systemd: + name: "mealie.service" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + changed_when: false + listen: "notification_restart_mealie" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/app/tasks/node/set_data_volume.yaml b/ansible/roles/app/tasks/node/set_data_volume.yaml new file mode 100644 index 0000000..bb42766 --- /dev/null +++ b/ansible/roles/app/tasks/node/set_data_volume.yaml @@ -0,0 +1,41 @@ +--- +- name: Check app data volume filesystem already exists + ansible.builtin.command: + cmd: "blkid /dev/{{ vm['data_storage']['name'] }}" + register: is_app_data + changed_when: false + failed_when: false + become: true + +- name: Create ext4 filesystem on app data raw + ansible.builtin.command: + cmd: "mkfs.ext4 -F /dev/{{ vm['data_storage']['name'] }}" + when: is_app_data.rc != 0 + changed_when: true + become: true + +- name: Set data volume path + ansible.builtin.file: + path: "{{ node['home_path'] }}/data" + state: "directory" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0770" + +- name: Mount app data volume + ansible.posix.mount: + path: "{{ node['home_path'] }}/data" + src: "/dev/{{ vm['data_storage']['name'] }}" + opts: "noatime" + state: "mounted" + fstype: "ext4" + become: true + +- name: Set app data volume path permissions + ansible.builtin.file: + path: "{{ node['home_path'] }}/data" + state: "directory" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0770" + become: true diff --git a/ansible/roles/app/tasks/services/set_collabora.yaml b/ansible/roles/app/tasks/services/set_collabora.yaml index 9a6abd3..a1ae5bd 100644 --- a/ansible/roles/app/tasks/services/set_collabora.yaml +++ b/ansible/roles/app/tasks/services/set_collabora.yaml @@ -4,7 +4,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/collabora/collabora.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/collabora.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_collabora" diff --git a/ansible/roles/app/tasks/services/set_gitea.yaml b/ansible/roles/app/tasks/services/set_gitea.yaml index 24ee5f9..4daa3b3 100644 --- a/ansible/roles/app/tasks/services/set_gitea.yaml +++ b/ansible/roles/app/tasks/services/set_gitea.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['gitea']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/gitea" @@ -18,7 +18,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/gitea/ssl/{{ root_cert_filename }}" owner: "{{ services['gitea']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_gitea" @@ -38,7 +38,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/gitea/gitea.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/gitea.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_gitea" diff --git a/ansible/roles/app/tasks/services/set_immich.yaml b/ansible/roles/app/tasks/services/set_immich.yaml index 2b41a54..3630863 100644 --- a/ansible/roles/app/tasks/services/set_immich.yaml +++ b/ansible/roles/app/tasks/services/set_immich.yaml @@ -8,7 +8,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['redis']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "containers/redis" @@ -21,7 +21,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_conf" @@ -30,7 +30,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_containerfile" @@ -56,7 +56,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/immich" @@ -71,7 +71,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/immich/ssl/{{ root_cert_filename }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" notify: "notification_restart_immich" no_log: true @@ -90,7 +90,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/immich/immich.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/immich.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_immich" @@ -99,7 +99,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/immich/immich-ml.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/immich-ml.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_immich-ml" diff --git a/ansible/roles/app/tasks/services/set_mealie.yaml b/ansible/roles/app/tasks/services/set_mealie.yaml new file mode 100644 index 0000000..b44ce3c --- /dev/null +++ b/ansible/roles/app/tasks/services/set_mealie.yaml @@ -0,0 +1,77 @@ +--- +- name: Create mealie directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ services['mealie']['subuid'] }}" + group: "{{ node['group'] }}" + mode: "0770" + loop: + - "data/containers/mealie" + - "data/containers/mealie/data" + - "containers/mealie" + - "containers/mealie/ssl" + become: true + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/mealie/ssl/{{ root_cert_filename }}" + owner: "{{ services['mealie']['subuid'] }}" + group: "{{ node['group'] }}" + mode: "0440" + become: true + notify: "notification_restart_mealie" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "MEALIE_POSTGRES_PASSWORD" + value: "{{ hostvars['console']['postgresql']['password']['mealie'] }}" + - name: "MEALIE_OIDC_CLIENT_SECRET" + value: "{{ hostvars['console']['mealie']['oidc']['secret'] }}" + notify: "notification_restart_mealie" + no_log: true + +- name: Deploy mealie.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/mealie/mealie.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/mealie.container" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0644" + notify: "notification_restart_mealie" + +- name: Enable mealie.service + ansible.builtin.systemd: + name: "mealie.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" + +- name: Check container archive images + ansible.builtin.stat: + path: "{{ node['home_path'] }}/archives/containers/ghcr.io_mealie-recipes_mealie_{{ version['containers']['mealie'] }}.tar" + register: container_archive_images + +- name: Save container archive images + containers.podman.podman_save: + image: + - "ghcr.io/mealie-recipes/mealie:{{ version['containers']['mealie'] }}" + dest: "{{ node['home_path'] }}/archives/containers/ghcr.io_mealie-recipes_mealie_{{ version['containers']['mealie'] }}.tar" + format: "oci-archive" + force: false + when: not container_archive_images.stat.exists + +- name: Fetch container archive images + ansible.builtin.fetch: + src: "{{ node['home_path'] }}/archives/containers/ghcr.io_mealie-recipes_mealie_{{ version['containers']['mealie'] }}.tar" + dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/" + flat: true diff --git a/ansible/roles/app/tasks/services/set_memos.yaml b/ansible/roles/app/tasks/services/set_memos.yaml index 921c6bc..cb10d6c 100644 --- a/ansible/roles/app/tasks/services/set_memos.yaml +++ b/ansible/roles/app/tasks/services/set_memos.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['memos']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/memos" @@ -19,7 +19,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/memos/ssl/{{ root_cert_filename }}" owner: "{{ services['memos']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_memos" @@ -33,6 +33,7 @@ sslrootcert=/etc/ssl/memos/{{ root_cert_filename }}" state: "present" force: true + no_log: true notify: "notification_restart_memos" - name: Deploy memos.container file @@ -40,7 +41,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/memos/memos.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/memos.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_memos" diff --git a/ansible/roles/app/tasks/services/set_nextcloud.yaml b/ansible/roles/app/tasks/services/set_nextcloud.yaml index bca5cca..7fbecda 100644 --- a/ansible/roles/app/tasks/services/set_nextcloud.yaml +++ b/ansible/roles/app/tasks/services/set_nextcloud.yaml @@ -8,7 +8,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['redis']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "containers/redis" @@ -21,7 +21,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_conf" @@ -30,7 +30,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_containerfile" @@ -48,7 +48,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['nextcloud']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/nextcloud" @@ -69,7 +69,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/nextcloud/ssl/{{ root_cert_filename }}" owner: "{{ services['nextcloud']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_nextcloud" @@ -88,8 +88,8 @@ detach: false env: NEXTCLOUD_UPDATE: "1" - NEXTCLOUD_ADMIN_USER: "admin-local" - NEXTCLOUD_ADMIN_PASSWORD: "{{ hostvars['console']['nextcloud']['admin-local']['password'] }}" + NEXTCLOUD_ADMIN_USER: "{{ hostvars['console']['nextcloud']['admin']['id'] }}" + NEXTCLOUD_ADMIN_PASSWORD: "{{ hostvars['console']['nextcloud']['admin']['password'] }}" POSTGRES_HOST: "{{ services['postgresql']['domain'] }}.{{ domain['internal'] }}:{{ services['postgresql']['ports']['tcp'] }}" POSTGRES_DB: "nextcloud_db" POSTGRES_USER: "nextcloud" @@ -110,14 +110,14 @@ state: "touch" mode: "0644" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" - name: Deploy config files ansible.builtin.template: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/nextcloud/config/{{ item }}.j2" dest: "{{ node['home_path'] }}/data/containers/nextcloud/html/config/{{ item }}" owner: "{{ services['nextcloud']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0640" loop: - "background.config.php" @@ -132,7 +132,7 @@ ansible.builtin.copy: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/nextcloud/ini/{{ item }}" dest: "{{ node['home_path'] }}/containers/nextcloud/ini/{{ item }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" loop: - "opcache.ini" @@ -144,7 +144,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/nextcloud/nextcloud.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/nextcloud.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_nextcloud" @@ -153,7 +153,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/nextcloud/systemd/{{ item }}" dest: "{{ node['home_path'] }}/.config/systemd/user/{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" loop: - "nextcloud-cron.service" diff --git a/ansible/roles/app/tasks/services/set_outline.yaml b/ansible/roles/app/tasks/services/set_outline.yaml index dbb1c75..3e7690e 100644 --- a/ansible/roles/app/tasks/services/set_outline.yaml +++ b/ansible/roles/app/tasks/services/set_outline.yaml @@ -8,7 +8,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['redis']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "containers/redis" @@ -21,7 +21,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_conf" @@ -30,7 +30,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_containerfile" @@ -48,7 +48,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['outline']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/outline" @@ -68,7 +68,7 @@ state: "touch" mode: "0644" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" when: not is_outline_init.stat.exists - name: Deploy root certificate @@ -77,7 +77,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/outline/ssl/{{ root_cert_filename }}" owner: "{{ services['outline']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_outline" @@ -106,7 +106,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/outline/outline.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/outline.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_outline" diff --git a/ansible/roles/app/tasks/services/set_paperless.yaml b/ansible/roles/app/tasks/services/set_paperless.yaml index a5a9303..c135461 100644 --- a/ansible/roles/app/tasks/services/set_paperless.yaml +++ b/ansible/roles/app/tasks/services/set_paperless.yaml @@ -8,7 +8,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['redis']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "containers/redis" @@ -21,7 +21,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_conf" @@ -30,7 +30,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_containerfile" @@ -48,7 +48,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['paperless']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/paperless" @@ -65,7 +65,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/paperless/build/paperless.containerfile.j2" dest: "{{ node['home_path'] }}/containers/paperless/build/Containerfile" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0640" - name: Deploy root certificate @@ -74,7 +74,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/paperless/ssl/{{ root_cert_filename }}" owner: "{{ services['paperless']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_paperless" @@ -131,7 +131,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/paperless/paperless.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/paperless.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_paperless" diff --git a/ansible/roles/app/tasks/services/set_sure.yaml b/ansible/roles/app/tasks/services/set_sure.yaml index 0558738..9f2fd49 100644 --- a/ansible/roles/app/tasks/services/set_sure.yaml +++ b/ansible/roles/app/tasks/services/set_sure.yaml @@ -8,7 +8,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['redis']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "containers/redis" @@ -21,7 +21,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_conf" @@ -30,7 +30,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" register: "is_redis_containerfile" @@ -48,7 +48,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['sure']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/sure" @@ -64,7 +64,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/sure/ssl/{{ root_cert_filename }}" owner: "{{ services['sure']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_sure" @@ -91,7 +91,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/sure/{{ item }}.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" loop: - "sure-web.container" diff --git a/ansible/roles/app/tasks/services/set_vaultwarden.yaml b/ansible/roles/app/tasks/services/set_vaultwarden.yaml index d5e4201..cdef9df 100644 --- a/ansible/roles/app/tasks/services/set_vaultwarden.yaml +++ b/ansible/roles/app/tasks/services/set_vaultwarden.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "data/containers/vaultwarden" @@ -17,7 +17,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/vaultwarden/ssl/{{ root_cert_filename }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_vaultwarden" @@ -44,7 +44,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/vaultwarden/vaultwarden.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/vaultwarden.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_vaultwarden" diff --git a/ansible/roles/auth/tasks/services/set_authelia.yaml b/ansible/roles/auth/tasks/services/set_authelia.yaml index e38b1e2..f735cc8 100644 --- a/ansible/roles/auth/tasks/services/set_authelia.yaml +++ b/ansible/roles/auth/tasks/services/set_authelia.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/containers/{{ item }}" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "authelia" @@ -17,7 +17,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/auth/authelia/config/authelia.yaml.j2" dest: "{{ node['home_path'] }}/containers/authelia/config/authelia.yaml" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" become: true notify: "notification_restart_authelia" @@ -29,7 +29,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/authelia/certs/{{ root_cert_filename }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true no_log: true @@ -65,7 +65,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/auth/authelia/authelia.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/authelia.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_authelia" diff --git a/ansible/roles/common/tasks/node/create_default_dir.yaml b/ansible/roles/common/tasks/node/create_default_dir.yaml index 467fc38..6a51744 100644 --- a/ansible/roles/common/tasks/node/create_default_dir.yaml +++ b/ansible/roles/common/tasks/node/create_default_dir.yaml @@ -22,7 +22,7 @@ path: "{{ node['home_path'] }}/.config/systemd/user" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0700" - name: Create quadlet directory @@ -30,5 +30,14 @@ path: "{{ node['home_path'] }}/.config/containers/systemd" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0700" + +- name: Set data volume path + ansible.builtin.file: + path: "{{ node['home_path'] }}/data" + state: "directory" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0770" + when: node['name'] in ["infra", "app"] diff --git a/ansible/roles/common/tasks/node/set_networkd.yaml b/ansible/roles/common/tasks/node/set_networkd.yaml index beaf97f..7b4d808 100644 --- a/ansible/roles/common/tasks/node/set_networkd.yaml +++ b/ansible/roles/common/tasks/node/set_networkd.yaml @@ -11,7 +11,7 @@ - name: Deploy networkd files ansible.builtin.template: src: "{{ item }}" - dest: "/etc/systemd/network/{{ item | basename }}" + dest: "/etc/systemd/network/{{ item | basename | replace('.j2', '') }}" owner: "root" group: "systemd-network" mode: "0640" diff --git a/ansible/roles/common/tasks/services/set_alloy.yaml b/ansible/roles/common/tasks/services/set_alloy.yaml index 34424b9..732d7ad 100644 --- a/ansible/roles/common/tasks/services/set_alloy.yaml +++ b/ansible/roles/common/tasks/services/set_alloy.yaml @@ -1,5 +1,5 @@ --- -- name: Gather system facts (hardware) +- name: Gather ystem facts (hardware) ansible.builtin.setup: gather_subset: - hardware @@ -67,6 +67,19 @@ become: true notify: "notification_restart_alloy" +- name: Set ACL for data directory + ansible.posix.acl: + path: "{{ item }}" + entity: "alloy" + etype: "user" + permissions: "x" + state: "present" + loop: + - "{{ node['home_path'] }}" + - "{{ node['home_path'] }}/data" + become: true + when: node['name'] == "app" + - name: Enable alloy service ansible.builtin.systemd: name: "alloy.service" diff --git a/ansible/roles/common/tasks/services/set_caddy.yaml b/ansible/roles/common/tasks/services/set_caddy.yaml index 8d584aa..b32b9c9 100644 --- a/ansible/roles/common/tasks/services/set_caddy.yaml +++ b/ansible/roles/common/tasks/services/set_caddy.yaml @@ -4,7 +4,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/containers/{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0770" loop: @@ -18,7 +18,7 @@ ansible.builtin.file: path: /var/log/caddy owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0755" become: true @@ -48,7 +48,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/common/caddy/build/caddy.containerfile.j2" dest: "{{ node['home_path'] }}/containers/caddy/build/Containerfile" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0640" - name: Deploy root crt for build @@ -56,7 +56,7 @@ content: "{{ hostvars['console']['ca']['root']['crt'] }}" dest: "{{ node['home_path'] }}/containers/caddy/build/{{ root_cert_filename }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0640" no_log: true @@ -77,7 +77,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/common/caddy/etc/{{ node['name'] }}/Caddyfile.j2" dest: "{{ node['home_path'] }}/containers/caddy/etc/Caddyfile" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" notify: "notification_restart_caddy" @@ -86,7 +86,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/common/caddy/caddy.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/caddy.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_caddy" diff --git a/ansible/roles/common/tasks/services/set_crowdsec.yaml b/ansible/roles/common/tasks/services/set_crowdsec.yaml index 8e078f8..23f3c51 100644 --- a/ansible/roles/common/tasks/services/set_crowdsec.yaml +++ b/ansible/roles/common/tasks/services/set_crowdsec.yaml @@ -315,7 +315,3 @@ enabled: true daemon_reload: true become: true - -# cscli bouncers list -# cscli machines list -# cscli metrics diff --git a/ansible/roles/common/tasks/services/set_kopia.yaml b/ansible/roles/common/tasks/services/set_kopia.yaml index 9dc921d..41c8875 100644 --- a/ansible/roles/common/tasks/services/set_kopia.yaml +++ b/ansible/roles/common/tasks/services/set_kopia.yaml @@ -14,7 +14,7 @@ kopia_{{ version['packages']['kopia'] }}_linux_{{ item }}.deb" dest: "{{ node['data_path'] }}/bin/kopia-{{ version['packages']['kopia'] }}-{{ item }}.deb" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: - "amd64" @@ -36,7 +36,7 @@ - name: Connect console kopia server environment: - KOPIA_PASSWORD: "{{ hostvars['console']['kopia']['user']['console'] }}" + KOPIA_PASSWORD: "{{ hostvars['console']['kopia']['users']['console'] }}" ansible.builtin.shell: | /usr/bin/kopia repository connect server \ --url=https://{{ services['kopia']['domain'] }}.{{ domain['internal'] }}:{{ services['kopia']['ports']['https'] }} \ diff --git a/ansible/roles/common/tasks/services/set_podman.yaml b/ansible/roles/common/tasks/services/set_podman.yaml index 1fedb3e..53124eb 100644 --- a/ansible/roles/common/tasks/services/set_podman.yaml +++ b/ansible/roles/common/tasks/services/set_podman.yaml @@ -11,7 +11,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/containers" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0700" @@ -19,7 +19,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/data/containers" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0770" when: node['name'] == "app" @@ -28,7 +28,7 @@ ansible.builtin.file: path: "{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0700" loop: diff --git a/ansible/roles/console/tasks/node/create_vmm_init.yaml b/ansible/roles/console/tasks/node/create_vmm_init.yaml new file mode 100644 index 0000000..9167be9 --- /dev/null +++ b/ansible/roles/console/tasks/node/create_vmm_init.yaml @@ -0,0 +1,90 @@ +--- +- name: Create vmm_init directory + ansible.builtin.file: + path: "{{ node['data_path'] }}/{{ item }}" + state: "directory" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0750" + loop: + - "vmm_init" + - "vmm_init/grub.d" + - "vmm_init/modprobe.d" + - "vmm_init/networkd" + - "vmm_init/ssh" + - "vmm_init/ssh/sshd_config.d" + - "vmm_init/sysctl.d" + +- name: Create nftables.conf + ansible.builtin.template: + src: "{{ node['config_path'] }}/node/vmm/nftables.conf.j2" + dest: "{{ node['data_path'] }}/vmm_init/nftables.conf" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + +- name: Create grub.d iommu config + ansible.builtin.copy: + content: | + GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt" + dest: "{{ node['data_path'] }}/vmm_init/grub.d/iommu.cfg" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + +- name: Create modprobe.d vfio config + ansible.builtin.copy: + content: | + options vfio-pci ids={{ hostvars['vmm']['hardware']['igpu']['id'] }} + softdep i915 pre: vfio-pci + # softdep ahci pre: vfio-pci # sata controller + dest: "{{ node['data_path'] }}/vmm_init/modprobe.d/vfio.conf" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + +- name: Create networkd network config + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ node['data_path'] }}/vmm_init/networkd/{{ item | basename | replace('.j2', '') }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + loop: "{{ query('fileglob', hostvars['console']['node']['config_path'] + '/node/vmm/networkd/*') | sort }}" + +- name: Create ssh CA + ansible.builtin.copy: + content: | + {{ hostvars['console']['ssh']['ca']['pub'] }} + dest: "{{ node['data_path'] }}/vmm_init/ssh/local_ssh_ca.pub" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + +- name: Create ssh config + ansible.builtin.copy: + content: "{{ item.value }}" + dest: "{{ node['data_path'] }}/vmm_init/ssh/sshd_config.d/{{ item.name }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" + loop: + - name: "prohibit_root.conf" + value: | + PermitRootLogin no + - name: "ssh_ca.conf" + value: | + TrustedUserCAKeys /etc/ssh/local_ssh_ca.pub + loop_control: + label: "{{ item.name }}" + +- name: Create sysctl.d bridge config + ansible.builtin.copy: + content: | + net.bridge.bridge-nf-call-ip6tables = 0 + net.bridge.bridge-nf-call-iptables = 0 + net.bridge.bridge-nf-call-arptables = 0 + dest: "{{ node['data_path'] }}/vmm_init/sysctl.d/bridge.conf" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0640" diff --git a/ansible/roles/console/tasks/node/load_secret_vars.yaml b/ansible/roles/console/tasks/node/load_secret_vars.yaml index 6048b9b..3380b7c 100644 --- a/ansible/roles/console/tasks/node/load_secret_vars.yaml +++ b/ansible/roles/console/tasks/node/load_secret_vars.yaml @@ -22,7 +22,7 @@ run_once: true no_log: true -- name: Load decrypted secret vaules in console +- name: Load decrypted secret values in console ansible.builtin.set_fact: "{{ item.key }}": "{{ item.value }}" loop: "{{ decrypted_secrets.stdout | from_yaml | dict2items }}" diff --git a/ansible/roles/console/tasks/node/set_ssh_client.yaml b/ansible/roles/console/tasks/node/set_ssh_client.yaml index d63095a..65b6daa 100644 --- a/ansible/roles/console/tasks/node/set_ssh_client.yaml +++ b/ansible/roles/console/tasks/node/set_ssh_client.yaml @@ -66,8 +66,8 @@ content: | {{ hostvars['console']['ssh']['ca']['key'] }} dest: "/run/user/{{ node['uid'] }}/local_ssh_ca_private_key" - owner: "console" - group: "svadmins" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" mode: "0400" no_log: true - name: Sign on ssh console key (pub file) @@ -91,7 +91,7 @@ path: "{{ node['home_path'] }}/.ssh" state: "directory" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0700" - name: Create ssh config file @@ -105,5 +105,5 @@ {% endfor %} dest: "{{ node['home_path'] }}/.ssh/config" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" diff --git a/ansible/roles/console/tasks/services/set_cli_tools.yaml b/ansible/roles/console/tasks/services/set_cli_tools.yaml index 53fdbba..9f2551a 100644 --- a/ansible/roles/console/tasks/services/set_cli_tools.yaml +++ b/ansible/roles/console/tasks/services/set_cli_tools.yaml @@ -31,7 +31,7 @@ sops_{{ version['packages']['sops'] }}_{{ item }}.deb" dest: "{{ node['data_path'] }}/bin/sops-{{ version['packages']['sops'] }}-{{ item }}.deb" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: - "amd64" @@ -43,7 +43,7 @@ step-cli_{{ version['packages']['step'] }}-1_{{ item }}.deb" dest: "{{ node['data_path'] }}/bin/step-{{ version['packages']['step'] }}-{{ item }}.deb" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: - "amd64" diff --git a/ansible/roles/fw/handlers/main.yaml b/ansible/roles/fw/handlers/main.yaml index 58c79e6..179945c 100644 --- a/ansible/roles/fw/handlers/main.yaml +++ b/ansible/roles/fw/handlers/main.yaml @@ -44,7 +44,7 @@ ansible.builtin.systemd: name: "blocky.service" state: "restarted" - enabled: "true" + enabled: true daemon_reload: true become: true changed_when: false diff --git a/ansible/roles/fw/tasks/services/set_bind.yaml b/ansible/roles/fw/tasks/services/set_bind.yaml index 6ba6334..f606f45 100644 --- a/ansible/roles/fw/tasks/services/set_bind.yaml +++ b/ansible/roles/fw/tasks/services/set_bind.yaml @@ -25,7 +25,7 @@ become: true when: is_bind_installed.rc != 0 -- name: Deploy acem.key +- name: Deploy acme.key ansible.builtin.copy: content: "{{ hostvars['console']['bind']['acme_key'] }}" dest: "/etc/bind/acme.key" diff --git a/ansible/roles/fw/tasks/services/set_ddns.yaml b/ansible/roles/fw/tasks/services/set_ddns.yaml index a8b07d0..55da4ab 100644 --- a/ansible/roles/fw/tasks/services/set_ddns.yaml +++ b/ansible/roles/fw/tasks/services/set_ddns.yaml @@ -6,7 +6,7 @@ API_KEY={{ hostvars['console']['ddns']['api_key'] }} dest: "/etc/secrets/{{ node['uid'] }}/ddns.env" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0400" become: true no_log: true @@ -16,7 +16,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/systemd/fw/ddns/ddns.sh" dest: "/usr/local/bin" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0711" become: true @@ -25,7 +25,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/systemd/fw/ddns/{{ item }}.j2" dest: "{{ node['home_path'] }}/.config/systemd/user/{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" validate: "/usr/bin/systemd-analyze verify %s" loop: diff --git a/ansible/roles/fw/tasks/services/set_kea.yaml b/ansible/roles/fw/tasks/services/set_kea.yaml index 7d23006..7c3ef3d 100644 --- a/ansible/roles/fw/tasks/services/set_kea.yaml +++ b/ansible/roles/fw/tasks/services/set_kea.yaml @@ -26,7 +26,7 @@ become: true notify: "notification_restart_kea4" -- name: Create kea-dhcp-server.service.d +- name: Create kea-dhcp4-server.service.d ansible.builtin.file: path: "/etc/systemd/system/kea-dhcp4-server.service.d" state: "directory" @@ -35,7 +35,7 @@ mode: "0755" become: true -- name: Set kea-dhcp-server.service.d/override.conf +- name: Set kea-dhcp4-server.service.d/override.conf ansible.builtin.copy: dest: "/etc/systemd/system/kea-dhcp4-server.service.d/override.conf" content: | diff --git a/ansible/roles/infra/handlers/main.yaml b/ansible/roles/infra/handlers/main.yaml index 7631c0b..2586a99 100644 --- a/ansible/roles/infra/handlers/main.yaml +++ b/ansible/roles/infra/handlers/main.yaml @@ -2,7 +2,7 @@ ansible.builtin.systemd: name: "ca.service" state: "restarted" - enabled: "true" + enabled: true daemon_reload: true scope: "user" changed_when: false diff --git a/ansible/roles/infra/tasks/services/set_ca_server.yaml b/ansible/roles/infra/tasks/services/set_ca_server.yaml index e4c7dc1..967c266 100644 --- a/ansible/roles/infra/tasks/services/set_ca_server.yaml +++ b/ansible/roles/infra/tasks/services/set_ca_server.yaml @@ -3,7 +3,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/containers/{{ item }}" owner: "{{ services['ca']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0770" loop: @@ -29,7 +29,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/ca/config/{{ item }}.j2" dest: "{{ node['home_path'] }}/containers/ca/config/{{ item }}" owner: "{{ services['ca']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0400" loop: - "ca.json" @@ -43,7 +43,7 @@ {{ item.value }} dest: "{{ item.path }}/{{ item.name }}" owner: "{{ services['ca']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "{{ item.mode }}" loop: - name: "{{ root_cert_filename }}" @@ -67,7 +67,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/ca/ca.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/ca.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_ca" diff --git a/ansible/roles/infra/tasks/services/set_grafana.yaml b/ansible/roles/infra/tasks/services/set_grafana.yaml index 123e25e..744ac21 100644 --- a/ansible/roles/infra/tasks/services/set_grafana.yaml +++ b/ansible/roles/infra/tasks/services/set_grafana.yaml @@ -3,7 +3,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/containers/{{ item }}" owner: "{{ services['grafana']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0770" loop: @@ -11,6 +11,7 @@ - "grafana/data" - "grafana/etc" - "grafana/etc/provisioning" + - "grafana/etc/provisioning/datasources" - "grafana/etc/dashboards" - "grafana/ssl" become: true @@ -21,7 +22,7 @@ {{ hostvars['console']['ca']['root']['crt'] }} dest: "{{ node['home_path'] }}/containers/grafana/ssl/{{ root_cert_filename }}" owner: "{{ services['grafana']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0400" become: true notify: "notification_restart_grafana" @@ -39,16 +40,16 @@ - name: "LDAP_BIND_PASSWORD" value: "{{ hostvars['console']['ldap']['password']['grafana'] }}" - name: "GF_ADMIN_PASSWORD" - value: "{{ hostvars['console']['grafana']['user']['password'] }}" + value: "{{ hostvars['console']['grafana']['admin']['password'] }}" notify: "notification_restart_grafana" no_log: true -- name: Deploy configruation files +- name: Deploy configuration files ansible.builtin.template: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/grafana/etc/{{ item }}.j2" dest: "{{ node['home_path'] }}/containers/grafana/etc/{{ item }}" owner: "{{ services['grafana']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0400" loop: - "grafana.ini" @@ -62,7 +63,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/grafana/etc/provisioning/datasources/datasources.yaml.j2" dest: "{{ node['home_path'] }}/containers/grafana/etc/provisioning/datasources/datasources.yaml" owner: "{{ services['grafana']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0400" become: true notify: "notification_restart_grafana" @@ -72,7 +73,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/grafana/grafana.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/grafana.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_grafana" diff --git a/ansible/roles/infra/tasks/services/set_ldap.yaml b/ansible/roles/infra/tasks/services/set_ldap.yaml index 1cecbe8..52d78af 100644 --- a/ansible/roles/infra/tasks/services/set_ldap.yaml +++ b/ansible/roles/infra/tasks/services/set_ldap.yaml @@ -3,7 +3,7 @@ ansible.builtin.file: path: "{{ node['home_path'] }}/containers/{{ item }}" owner: "{{ services['ldap']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" state: "directory" mode: "0770" loop: @@ -18,7 +18,7 @@ {{ item.value }} dest: "{{ node['home_path'] }}/containers/ldap/ssl/{{ item.name }}" owner: "{{ services['ldap']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "{{ item.mode }}" loop: - name: "{{ root_cert_filename }}" @@ -33,8 +33,8 @@ value: "{{ hostvars['console']['ldap']['key'] }}" mode: "0400" become: true - notify: "notification_restart_ldap" no_log: true + notify: "notification_restart_ldap" - name: Register secret value to podman secret containers.podman.podman_secret: @@ -51,14 +51,15 @@ value: "{{ hostvars['console']['ldap']['seed_key'] }}" - name: "LLDAP_JWT_SECRET" value: "{{ hostvars['console']['ldap']['jwt_secret'] }}" - notify: "notification_restart_ldap" no_log: true + notify: "notification_restart_ldap" - name: Initiate ldap (When = false, If DB data does not exist in postgresql, activate this block) # The reason why this task doesn't use the way to check ".init" file is this tasks can override original database. # Absent of ".init" file cannot guarantee DB is empty. when: false become: true + no_log: true block: - name: Register extra secret value to podman secret containers.podman.podman_secret: @@ -97,7 +98,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/ldap/ldap.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/ldap.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_ldap" diff --git a/ansible/roles/infra/tasks/services/set_loki.yaml b/ansible/roles/infra/tasks/services/set_loki.yaml index 5d9b7ba..9910872 100644 --- a/ansible/roles/infra/tasks/services/set_loki.yaml +++ b/ansible/roles/infra/tasks/services/set_loki.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/containers/{{ item }}" state: "directory" owner: "{{ services['loki']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "loki" @@ -18,7 +18,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/loki/etc/loki.yaml.j2" dest: "{{ node['home_path'] }}/containers/loki/etc/loki.yaml" owner: "{{ services['loki']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" become: true notify: "notification_restart_loki" @@ -30,7 +30,7 @@ {{ item.value }} dest: "{{ node['home_path'] }}/containers/loki/ssl/{{ item.name }}" owner: "{{ services['loki']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "{{ item.mode }}" loop: - name: "{{ root_cert_filename }}" @@ -53,7 +53,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/loki/loki.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/loki.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_loki" diff --git a/ansible/roles/infra/tasks/services/set_postgresql.yaml b/ansible/roles/infra/tasks/services/set_postgresql.yaml index bfde8f6..e956fb9 100644 --- a/ansible/roles/infra/tasks/services/set_postgresql.yaml +++ b/ansible/roles/infra/tasks/services/set_postgresql.yaml @@ -3,33 +3,33 @@ ansible.builtin.set_fact: connected_services: - "ldap" - - "authelia" - "grafana" + - "authelia" - "vaultwarden" - "gitea" - "immich" - "paperless" - - "affine" - "nextcloud" - "sure" - "outline" - "memos" + - "mealie" - name: Create postgresql directory ansible.builtin.file: - path: "{{ node['home_path'] }}/containers/{{ item }}" + path: "{{ node['home_path'] }}/{{ item }}" state: "directory" owner: "{{ services['postgresql']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - - "postgresql" - - "postgresql/data" - - "postgresql/config" - - "postgresql/ssl" - - "postgresql/init" - - "postgresql/backups" - - "postgresql/build" + - "data/containers/postgresql/backups" + - "containers/postgresql" + - "containers/postgresql/data" + - "containers/postgresql/config" + - "containers/postgresql/ssl" + - "containers/postgresql/init" + - "containers/postgresql/build" become: true - name: Deploy containerfile for build @@ -37,7 +37,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/build/postgresql.containerfile.j2" dest: "{{ node['home_path'] }}/containers/postgresql/build/Containerfile" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0640" - name: Build postgresql container image @@ -57,7 +57,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/config/{{ item }}.j2" dest: "{{ node['home_path'] }}/containers/postgresql/config/{{ item }}" owner: "{{ services['postgresql']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: - "postgresql.conf" @@ -72,7 +72,7 @@ {{ item.value }} dest: "{{ node['home_path'] }}/containers/postgresql/ssl/{{ item.name }}" owner: "{{ services['postgresql']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "{{ item.mode }}" loop: - name: "{{ root_cert_filename }}" @@ -97,24 +97,26 @@ register: "is_postgresql_init" - name: Prepare initiating DB - when: not is_postgresql_init.stat.exists + when: + - not is_postgresql_init.stat.exists + - (connected_services | default([]) | length) > 0 become: true block: # `init/pg_cluster.sql` should be fetched from postgresql's backup directory before running initiating - name: Deploy init cluster sql file ansible.builtin.copy: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/init/pg_cluster.sql" - dest: "{{ node['home_path'] }}/containers/postgresql/init/0_pg_cluster.sql" + dest: "{{ node['home_path'] }}/containers/postgresql/init/00_pg_cluster.sql" owner: "{{ services['postgresql']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" - name: Deploy restoring data sql files ansible.builtin.copy: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/init/pg_{{ item }}.sql" - dest: "{{ node['home_path'] }}/containers/postgresql/init/{{ index_num + 1 }}_pg_{{ item }}.sql" + dest: "{{ node['home_path'] }}/containers/postgresql/init/{{ '%02d' | format(index_num + 1) }}_pg_{{ item }}.sql" owner: "{{ services['postgresql']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: "{{ connected_services }}" loop_control: @@ -126,14 +128,14 @@ state: "touch" mode: "0644" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" - name: Deploy container file ansible.builtin.template: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/postgresql.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/postgresql.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_postgresql" @@ -142,7 +144,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/services/{{ item }}" dest: "{{ node['home_path'] }}/.config/systemd/user/{{ item }}" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" loop: - "postgresql-cluster-backup.service" diff --git a/ansible/roles/infra/tasks/services/set_prometheus.yaml b/ansible/roles/infra/tasks/services/set_prometheus.yaml index 1538add..95df464 100644 --- a/ansible/roles/infra/tasks/services/set_prometheus.yaml +++ b/ansible/roles/infra/tasks/services/set_prometheus.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/containers/{{ item }}" state: "directory" owner: "{{ services['prometheus']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "prometheus" @@ -18,7 +18,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/prometheus/etc/{{ item }}.j2" dest: "{{ node['home_path'] }}/containers/prometheus/etc/{{ item }}" owner: "{{ services['prometheus']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0600" loop: - "prometheus.yaml" @@ -34,7 +34,7 @@ {{ item.value }} dest: "{{ node['home_path'] }}/containers/prometheus/ssl/{{ item.name }}" owner: "{{ services['prometheus']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "{{ item.mode }}" loop: - name: "{{ root_cert_filename }}" @@ -57,7 +57,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/prometheus/prometheus.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/prometheus.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_prometheus" diff --git a/ansible/roles/infra/tasks/services/set_x509-exporter.yaml b/ansible/roles/infra/tasks/services/set_x509-exporter.yaml index 6bd9aec..a1496aa 100644 --- a/ansible/roles/infra/tasks/services/set_x509-exporter.yaml +++ b/ansible/roles/infra/tasks/services/set_x509-exporter.yaml @@ -4,7 +4,7 @@ path: "{{ node['home_path'] }}/containers/{{ item }}" state: "directory" owner: "{{ services['x509-exporter']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0770" loop: - "x509-exporter" @@ -17,7 +17,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/x509-exporter/config/config.yaml" dest: "{{ node['home_path'] }}/containers/x509-exporter/config/config.yaml" owner: "{{ services['x509-exporter']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" become: true notify: "notification_restart_x509-exporter" @@ -28,7 +28,7 @@ {{ item.value }} dest: "{{ node['home_path'] }}/containers/x509-exporter/certs/{{ item.name }}" owner: "{{ services['x509-exporter']['subuid'] }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0440" loop: - name: "root.crt" @@ -57,7 +57,7 @@ src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/x509-exporter/x509-exporter.container.j2" dest: "{{ node['home_path'] }}/.config/containers/systemd/x509-exporter.container" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" notify: "notification_restart_x509-exporter" diff --git a/ansible/roles/vmm/tasks/node/set_libvirt.yaml b/ansible/roles/vmm/tasks/node/set_libvirt.yaml index 7f34aee..f3ca54c 100644 --- a/ansible/roles/vmm/tasks/node/set_libvirt.yaml +++ b/ansible/roles/vmm/tasks/node/set_libvirt.yaml @@ -9,15 +9,16 @@ - name: Check libvirt directory ansible.builtin.stat: - path: "/var/lib/libvirt/{{ item }}" + path: "{{ node['libvirt_path'] }}/{{ item }}" loop: - "images" - "seeds" + - "data" register: "is_libvirt_dir" - name: Create libvirt directory ansible.builtin.file: - path: "/var/lib/libvirt/{{ item.item }}" + path: "{{ node['libvirt_path'] }}/{{ item.item }}" state: "directory" owner: "root" group: "root" @@ -72,6 +73,7 @@ loop: - "images-pool" - "seeds-pool" + - "data-pool" - name: Start virtual storage pool community.libvirt.virt_pool: @@ -81,6 +83,7 @@ loop: - "images-pool" - "seeds-pool" + - "data-pool" - name: Autostart virtual storage pool community.libvirt.virt_pool: @@ -90,3 +93,4 @@ loop: - "images-pool" - "seeds-pool" + - "data-pool" diff --git a/ansible/roles/app/tasks/node/set_raid.yaml b/ansible/roles/vmm/tasks/node/set_raid.yaml similarity index 64% rename from ansible/roles/app/tasks/node/set_raid.yaml rename to ansible/roles/vmm/tasks/node/set_raid.yaml index ee0fee9..c4f0ccb 100644 --- a/ansible/roles/app/tasks/node/set_raid.yaml +++ b/ansible/roles/vmm/tasks/node/set_raid.yaml @@ -18,16 +18,17 @@ - name: Set hard disk path ansible.builtin.shell: | set -o pipefail - ls -1 /dev/disk/by-path/*{{ vm['pass_through']['sata_controller']['address'] }}* | \ - grep -v '\.0$' | \ + ls -1 /dev/disk/by-id/ata-* | \ + grep -F -e "{{ hardware['hdd']['data'].values() | join('" -e "') }}" | \ + grep -vE -- '-part[0-9]+$' | \ sort changed_when: false register: "hdd_path_list" -- name: Check app_hdd filesystem already exists +- name: Check vmm_hdd filesystem already exists ansible.builtin.command: | - blkid -L {{ storage['btrfs']['label'] }} - register: is_app_data + blkid -L {{ hardware['btrfs']['label'] }} + register: is_vmm_data changed_when: false failed_when: false become: true @@ -40,38 +41,47 @@ - name: Set btrfs raid10 volume ansible.builtin.shell: | mkfs.btrfs -f \ - -L {{ storage['btrfs']['label'] }} \ - -d {{ storage['btrfs']['level'] }} \ - -m {{ storage['btrfs']['level'] }} \ + -L {{ hardware['btrfs']['label'] }} \ + -d {{ hardware['btrfs']['level'] }} \ + -m {{ hardware['btrfs']['level'] }} \ {{ hdd_path_list.stdout_lines | join(' ') }} become: true when: - - is_app_data.rc != 0 + - is_vmm_data.rc != 0 - (hdd_path_list.stdout_lines | length) >= 4 changed_when: is_mkfs.rc == 0 register: "is_mkfs" -- name: Mount btrfs raid10 volume - ansible.posix.mount: - path: "{{ storage['btrfs']['mount_point'] }}" - src: "LABEL={{ storage['btrfs']['label'] }}" - state: "mounted" - fstype: "btrfs" - opts: "defaults,noatime,compress=zstd:3,autodefrag,degraded,nofail" +- name: Create mount volume + ansible.builtin.file: + path: "{{ hardware['btrfs']['mount_point'] }}" + state: "directory" + owner: "root" + group: "root" + mode: "0711" become: true -- name: Set hard disk path permissions +- name: Mount btrfs raid10 volume + ansible.posix.mount: + path: "{{ hardware['btrfs']['mount_point'] }}" + src: "LABEL={{ hardware['btrfs']['label'] }}" + state: "mounted" + fstype: "btrfs" + opts: "defaults,noatime,nodatacow,degraded,nofail" + become: true + +- name: Set mounted btrfs volume permissions ansible.builtin.file: - path: "{{ storage['btrfs']['mount_point'] }}" + path: "{{ hardware['btrfs']['mount_point'] }}" state: "directory" - owner: "{{ ansible_user }}" - group: "svadmins" - mode: "0770" + owner: "root" + group: "root" + mode: "0711" become: true - name: Deploy btrfs scrub service and timer ansible.builtin.template: - src: "{{ hostvars['console']['node']['config_path'] }}/services/systemd/app/btrfs/{{ item }}.j2" + src: "{{ hostvars['console']['node']['config_path'] }}/services/systemd/vmm/btrfs/{{ item }}.j2" dest: "/etc/systemd/system/{{ item }}" owner: "root" group: "root" diff --git a/ansible/roles/vmm/tasks/vm/create_seed.yaml b/ansible/roles/vmm/tasks/vm/create_seed.yaml index 29ff167..f5c2135 100644 --- a/ansible/roles/vmm/tasks/vm/create_seed.yaml +++ b/ansible/roles/vmm/tasks/vm/create_seed.yaml @@ -1,13 +1,13 @@ --- # This task is located in vmm roles because of its attributes, # but all process should be run in "console". -# At the playbook, `delegate_to: "console"` option is applyed by `apply:`. +# At the playbook, `delegate_to: "console"` option is applied by `apply:`. - name: Create images directory ansible.builtin.file: path: "{{ hostvars['console']['node']['data_path'] }}/images/seeds/{{ target_vm }}" state: "directory" - owner: "console" - group: "svadmins" + owner: "{{ hostvars['console']['node']['name'] }}" + group: "{{ hostvars['console']['node']['group'] }}" mode: "0700" - name: Create temp meta-data @@ -16,8 +16,8 @@ instance-id: vm-{{ target_vm }} local-hostname: {{ target_vm }} dest: "{{ hostvars['console']['node']['data_path'] }}/images/seeds/{{ target_vm }}/meta-data" - owner: "console" - group: "svadmins" + owner: "{{ hostvars['console']['node']['name'] }}" + group: "{{ hostvars['console']['node']['group'] }}" mode: "0600" register: "vm_meta_data" no_log: true @@ -26,8 +26,8 @@ ansible.builtin.template: src: "{{ hostvars['console']['node']['config_path'] }}/services/systemd/vmm/libvirt/seeds/user-data.j2" dest: "{{ hostvars['console']['node']['data_path'] }}/images/seeds/{{ target_vm }}/user-data" - owner: "console" - group: "svadmins" + owner: "{{ hostvars['console']['node']['name'] }}" + group: "{{ hostvars['console']['node']['group'] }}" mode: "0600" register: "vm_user_data" no_log: true @@ -37,8 +37,8 @@ content: | network: {config: disabled} dest: "{{ hostvars['console']['node']['data_path'] }}/images/seeds/{{ target_vm }}/network-config" - owner: "console" - group: "svadmins" + owner: "{{ hostvars['console']['node']['name'] }}" + group: "{{ hostvars['console']['node']['group'] }}" mode: "0600" register: "vm_network_config" no_log: true diff --git a/ansible/roles/vmm/tasks/vm/deploy_vm_init.yaml b/ansible/roles/vmm/tasks/vm/deploy_vm_init.yaml index 97b46b4..0536c3d 100644 --- a/ansible/roles/vmm/tasks/vm/deploy_vm_init.yaml +++ b/ansible/roles/vmm/tasks/vm/deploy_vm_init.yaml @@ -1,16 +1,16 @@ --- - name: Check vm cloud-init ansible.builtin.stat: - path: "/var/lib/libvirt/images/debian-13.qcow2" + path: "{{ node['libvirt_path'] }}/images/debian-13.qcow2" become: true register: is_cloud_init_file - name: Deploy vm cloud-init ansible.builtin.copy: src: "{{ hostvars['console']['node']['data_path'] }}/images/debian-13-generic-amd64.qcow2" - dest: "/var/lib/libvirt/images/debian-13.qcow2" + dest: "{{ node['libvirt_path'] }}/images/debian-13.qcow2" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" force: false become: true @@ -18,10 +18,10 @@ - name: Remote copy vm cloud-init file ansible.builtin.copy: - src: "/var/lib/libvirt/images/debian-13.qcow2" - dest: "/var/lib/libvirt/images/{{ target_vm }}.qcow2" + src: "{{ node['libvirt_path'] }}/images/debian-13.qcow2" + dest: "{{ node['libvirt_path'] }}/images/{{ target_vm }}.qcow2" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" remote_src: true force: false @@ -29,7 +29,7 @@ - name: Check deployed cloud-init file info ansible.builtin.command: - cmd: "qemu-img info /var/lib/libvirt/images/{{ target_vm }}.qcow2 --output json" + cmd: "qemu-img info {{ node['libvirt_path'] }}/images/{{ target_vm }}.qcow2 --output json" changed_when: false failed_when: - deployed_cloudfile_info.rc != 0 @@ -38,18 +38,37 @@ - name: Resize deployed cloud-init file ansible.builtin.command: - cmd: "qemu-img resize /var/lib/libvirt/images/{{ target_vm }}.qcow2 {{ hostvars[target_vm]['vm']['storage'] }}G" + cmd: "qemu-img resize {{ node['libvirt_path'] }}/images/{{ target_vm }}.qcow2 {{ hostvars[target_vm]['vm']['storage'] }}G" when: - deployed_cloudfile_info.rc == 0 - (deployed_cloudfile_info.stdout | from_json)['virtual-size'] < (hostvars[target_vm]['vm']['storage'] | int * 1024 * 1024 * 1024) changed_when: true +- name: Check app data raw image + ansible.builtin.stat: + path: "{{ hardware['btrfs']['mount_point'] }}/{{ target_vm }}.raw" + get_checksum: false + get_mime: false + get_attributes: false + when: target_vm == "app" + register: "is_app_data" + +- name: Create app data raw image + ansible.builtin.command: + # 3.5T + cmd: "qemu-img create -f raw {{ hardware['btrfs']['mount_point'] }}/{{ target_vm }}.raw {{ hostvars[target_vm]['vm']['data_storage']['size'] }}T" + become: true + when: + - target_vm == "app" + - not is_app_data.stat.exists + changed_when: true + - name: Deploy vm seed.iso ansible.builtin.copy: src: "{{ hostvars['console']['node']['data_path'] }}/images/seeds/{{ target_vm }}/seed.iso" - dest: "/var/lib/libvirt/seeds/{{ target_vm }}_seed.iso" + dest: "{{ node['libvirt_path'] }}/seeds/{{ target_vm }}_seed.iso" owner: "{{ ansible_user }}" - group: "svadmins" + group: "{{ node['group'] }}" mode: "0644" become: true when: deployed_cloudfile_info.rc == 0 diff --git a/config/node/common/hosts.j2 b/config/node/common/hosts.j2 index b38ccb8..2d07a90 100644 --- a/config/node/common/hosts.j2 +++ b/config/node/common/hosts.j2 @@ -16,6 +16,9 @@ {{ hostvars['fw']['network6']['infra']['server'] }} infra.{{ domain['internal'] }} {{ hostvars['fw']['network6']['auth']['server'] }} auth.{{ domain['internal'] }} {{ hostvars['fw']['network6']['app']['server'] }} app.{{ domain['internal'] }} +# kopia repository +{{ hostvars['fw']['network4']['nas']['client'] }} nas.{{ domain['internal'] }} +{{ hostvars['fw']['network6']['nas']['client'] }} nas.{{ domain['internal'] }} {% else %} # IPv4 # Crowdsec, blocky, bind(fw) diff --git a/config/node/fw/nftables.conf.j2 b/config/node/fw/nftables.conf.j2 index ff3a85f..dc9299b 100644 --- a/config/node/fw/nftables.conf.j2 +++ b/config/node/fw/nftables.conf.j2 @@ -78,7 +78,7 @@ table inet filter { set crowdsec6-blacklists { type ipv6_addr flags timeout - } + } chain global { # invalid packets ct state invalid drop comment "deny invalid connection" @@ -114,7 +114,7 @@ table inet filter { iifname "lo" accept comment "allow local connection: FW > FW" udp dport $PORTS_VPN accept comment "allow vpn connection: > FW" iifname { $IF_CLIENT, $IF_SERVER, $IF_USER, $IF_WG } meta l4proto { icmp, icmpv6 } accept comment "allow icmp, icmpv6 connection: CLIENT/SERVER/USER/WG > FW" - iifname { $IF_CLIENT, $IF_SERVER, $IF_USER, $IF_WG } udp dport $PORTS_NTP accept comment "allow ntp connection: CLIENT/SERVER/USER/WG > FW" + iifname { $IF_CLIENT, $IF_SERVER, $IF_WG } udp dport $PORTS_NTP accept comment "allow ntp connection: CLIENT/SERVER/USER/WG > FW" # Global chain contains "WAN !> :SSH_PORT" ip saddr $HOSTS4_CONSOLE tcp dport $PORTS_SSH accept comment "allow ipv4 ssh connection: CONSOLE > FW" ip6 saddr $HOSTS6_CONSOLE tcp dport $PORTS_SSH accept comment "allow ipv6 ssh connection: CONSOLE > FW" diff --git a/config/node/vmm/networkd/00-vmm-eth0.link b/config/node/vmm/networkd/00-vmm-eth0.link deleted file mode 100644 index 1119415..0000000 --- a/config/node/vmm/networkd/00-vmm-eth0.link +++ /dev/null @@ -1,5 +0,0 @@ -[Match] -MACAddress=c8:ff:bf:05:aa:b0 - -[Link] -Name=eth0 diff --git a/config/node/vmm/networkd/00-vmm-eth0.link.j2 b/config/node/vmm/networkd/00-vmm-eth0.link.j2 new file mode 100644 index 0000000..dfc554a --- /dev/null +++ b/config/node/vmm/networkd/00-vmm-eth0.link.j2 @@ -0,0 +1,5 @@ +[Match] +MACAddress={{ hostvars['vmm']['hardware']['mac']['nic0'] }} + +[Link] +Name=eth0 diff --git a/config/node/vmm/networkd/01-vmm-eth1.link b/config/node/vmm/networkd/01-vmm-eth1.link deleted file mode 100644 index 5f72662..0000000 --- a/config/node/vmm/networkd/01-vmm-eth1.link +++ /dev/null @@ -1,5 +0,0 @@ -[Match] -MACAddress=c8:ff:bf:05:aa:b1 - -[Link] -Name=eth1 diff --git a/config/node/vmm/networkd/01-vmm-eth1.link.j2 b/config/node/vmm/networkd/01-vmm-eth1.link.j2 new file mode 100644 index 0000000..89446aa --- /dev/null +++ b/config/node/vmm/networkd/01-vmm-eth1.link.j2 @@ -0,0 +1,5 @@ +[Match] +MACAddress={{ hostvars['vmm']['hardware']['mac']['nic1'] }} + +[Link] +Name=eth1 diff --git a/config/node/vmm/networkd/10-vmm-br0.netdev b/config/node/vmm/networkd/10-vmm-br0.netdev.j2 similarity index 100% rename from config/node/vmm/networkd/10-vmm-br0.netdev rename to config/node/vmm/networkd/10-vmm-br0.netdev.j2 diff --git a/config/node/vmm/networkd/11-vmm-br1.netdev b/config/node/vmm/networkd/11-vmm-br1.netdev.j2 similarity index 100% rename from config/node/vmm/networkd/11-vmm-br1.netdev rename to config/node/vmm/networkd/11-vmm-br1.netdev.j2 diff --git a/config/node/vmm/networkd/12-vmm-vlan1.netdev b/config/node/vmm/networkd/12-vmm-vlan1.netdev.j2 similarity index 100% rename from config/node/vmm/networkd/12-vmm-vlan1.netdev rename to config/node/vmm/networkd/12-vmm-vlan1.netdev.j2 diff --git a/config/node/vmm/networkd/13-vmm-vlan10.netdev b/config/node/vmm/networkd/13-vmm-vlan10.netdev.j2 similarity index 100% rename from config/node/vmm/networkd/13-vmm-vlan10.netdev rename to config/node/vmm/networkd/13-vmm-vlan10.netdev.j2 diff --git a/config/node/vmm/networkd/14-vmm-vlan20.netdev b/config/node/vmm/networkd/14-vmm-vlan20.netdev.j2 similarity index 100% rename from config/node/vmm/networkd/14-vmm-vlan20.netdev rename to config/node/vmm/networkd/14-vmm-vlan20.netdev.j2 diff --git a/config/node/vmm/networkd/20-vmm-eth0.network b/config/node/vmm/networkd/20-vmm-eth0.network.j2 similarity index 100% rename from config/node/vmm/networkd/20-vmm-eth0.network rename to config/node/vmm/networkd/20-vmm-eth0.network.j2 diff --git a/config/node/vmm/networkd/21-vmm-eth1.network b/config/node/vmm/networkd/21-vmm-eth1.network.j2 similarity index 100% rename from config/node/vmm/networkd/21-vmm-eth1.network rename to config/node/vmm/networkd/21-vmm-eth1.network.j2 diff --git a/config/node/vmm/networkd/22-vmm-br0.network b/config/node/vmm/networkd/22-vmm-br0.network.j2 similarity index 100% rename from config/node/vmm/networkd/22-vmm-br0.network rename to config/node/vmm/networkd/22-vmm-br0.network.j2 diff --git a/config/node/vmm/networkd/23-vmm-br1.network b/config/node/vmm/networkd/23-vmm-br1.network.j2 similarity index 100% rename from config/node/vmm/networkd/23-vmm-br1.network rename to config/node/vmm/networkd/23-vmm-br1.network.j2 diff --git a/config/node/vmm/networkd/24-vmm-vlan1.network b/config/node/vmm/networkd/24-vmm-vlan1.network deleted file mode 100644 index 3ebb2cd..0000000 --- a/config/node/vmm/networkd/24-vmm-vlan1.network +++ /dev/null @@ -1,28 +0,0 @@ -[Match] -Name=vlan1 - -[Network] -# IPv4 -Address=192.168.1.10/24 -# IPv6 -Address=fd00:1::10/64 - -[RoutingPolicyRule] -From=192.168.1.10/32 -Table=1 -Priority=100 - -[Route] -Destination=192.168.1.0/24 -Scope=link -Table=1 - -[RoutingPolicyRule] -From=fd00:1::10/128 -Table=61 -Priority=100 - -[Route] -Destination=fd00:1::/64 -Scope=link -Table=61 diff --git a/config/node/vmm/networkd/24-vmm-vlan1.network.j2 b/config/node/vmm/networkd/24-vmm-vlan1.network.j2 new file mode 100644 index 0000000..59caba5 --- /dev/null +++ b/config/node/vmm/networkd/24-vmm-vlan1.network.j2 @@ -0,0 +1,28 @@ +[Match] +Name=vlan1 + +[Network] +# IPv4 +Address={{ hostvars['fw']['network4']['vmm']['client'] }}/24 +# IPv6 +Address={{ hostvars['fw']['network6']['vmm']['client'] }}/64 + +[RoutingPolicyRule] +From={{ hostvars['fw']['network4']['vmm']['client'] }}/32 +Table=1 +Priority=100 + +[Route] +Destination={{ hostvars['fw']['network4']['subnet']['client'] }} +Scope=link +Table=1 + +[RoutingPolicyRule] +From={{ hostvars['fw']['network6']['vmm']['client'] }}/128 +Table=61 +Priority=100 + +[Route] +Destination={{ hostvars['fw']['network6']['subnet']['client'] }} +Scope=link +Table=61 diff --git a/config/node/vmm/networkd/25-vmm-vlan10.network b/config/node/vmm/networkd/25-vmm-vlan10.network deleted file mode 100644 index 62cc6e8..0000000 --- a/config/node/vmm/networkd/25-vmm-vlan10.network +++ /dev/null @@ -1,32 +0,0 @@ -[Match] -Name=vlan10 -[Network] -RequiredForOnline=false -# IPv4 -Address=192.168.10.10/24 -Gateway=192.168.10.1 -DNS=192.168.10.2 -# IPv6 -Address=fd00:10::10/64 -Gateway=fd00:10::1 -DNS=fd00:10::2 - -[RoutingPolicyRule] -From=192.168.10.10/32 -Table=2 -Priority=100 - -[Route] -Destination=0.0.0.0/0 -Gateway=192.168.10.1 -Table=2 - -[RoutingPolicyRule] -From=fd00:10::10/128 -Table=62 -Priority=100 - -[Route] -Destination=::/0 -Gateway=fd00:10::1 -Table=62 diff --git a/config/node/vmm/networkd/25-vmm-vlan10.network.j2 b/config/node/vmm/networkd/25-vmm-vlan10.network.j2 new file mode 100644 index 0000000..a0b46e4 --- /dev/null +++ b/config/node/vmm/networkd/25-vmm-vlan10.network.j2 @@ -0,0 +1,32 @@ +[Match] +Name=vlan10 +[Network] +RequiredForOnline=false +# IPv4 +Address={{ hostvars['fw']['network4']['vmm']['server'] }}/24 +Gateway={{ hostvars['fw']['network4']['firewall']['server'] }} +DNS={{ hostvars['fw']['network4']['blocky']['server'] }} +# IPv6 +Address={{ hostvars['fw']['network6']['vmm']['server'] }}/64 +Gateway={{ hostvars['fw']['network6']['firewall']['server'] }} +DNS={{ hostvars['fw']['network6']['blocky']['server'] }} + +[RoutingPolicyRule] +From={{ hostvars['fw']['network4']['vmm']['server'] }}/32 +Table=2 +Priority=100 + +[Route] +Destination=0.0.0.0/0 +Gateway={{ hostvars['fw']['network4']['firewall']['server'] }} +Table=2 + +[RoutingPolicyRule] +From={{ hostvars['fw']['network6']['vmm']['server'] }}/128 +Table=62 +Priority=100 + +[Route] +Destination=::/0 +Gateway={{ hostvars['fw']['network6']['firewall']['server'] }} +Table=62 diff --git a/config/secrets/edit_secret.sh b/config/secrets/edit_secret.sh index d8e9150..12a7db2 100755 --- a/config/secrets/edit_secret.sh +++ b/config/secrets/edit_secret.sh @@ -3,7 +3,7 @@ set -e -# Varibles +# Variables KEY_PATH="$HOME/workspace/homelab/config/secrets" TMP_PATH="/run/user/$UID" SECRET_FILE="$1" @@ -72,7 +72,7 @@ echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ --decrypt "$KEY_PATH/age-key.gpg" &&\ chmod 600 "$TMP_PATH/age-key" -# Unset environment varibles +# Unset environment Variables unset GPG_PASSPHRASE # Check the key on memory diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index 155936a..ca3f812 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -59,10 +59,10 @@ wireguard: #ENC[AES256_GCM,data:+kiy17Z7q3EiPgo4Ykk=,iv:vOi54XHHLNIO9U0ybpU9Cn68Ymj5W4vtQ2OhTkGAmRo=,tag:h3wdCo5UfNGlHqMpeeBf/A==,type:comment] kopia: repository: ENC[AES256_GCM,data:16V50bVVtecbF+TJcXHCx46ZPGNMgAhUOA0YbZHVTsCrOWQvTvcK2k8Iq3E=,iv:/wNbrE5GaFiQPNEp8wZ0JehbXkmah/gPm5ywNE6WVVM=,tag:vW82KTKtJ44++tqXON/SYw==,type:str] - user: - console: ENC[AES256_GCM,data:LZQEz7JS8CMMSlGB2/FS15VHAbko6YpCaWDZDa9tXVnmYLwFs8F39/u8kLI=,iv:WrYox/3LweqafWl96fxmezTFhKHB/2XSDSLwHljdpw0=,tag:bPwqkdeexXItkbpPQQ6klg==,type:str] - infra: ENC[AES256_GCM,data:fwvu96Zhao5RthZptxhuv2NobY1riQzpvD4Ardc8UTwN6+HyfFVR31yTTqk=,iv:jqFEKsP2npZgeRL3E3Va7oLzbHShBO4FxLRYwnfLtys=,tag:qXescWki6oPaaSrhMsUw9g==,type:str] - app: ENC[AES256_GCM,data:VVs8lfo8ZsbYfeKIwtaFCrpV82SguR/xxNnMZXXp8YOBP+6F2Vs5jeDtTTs=,iv:L2mS2RhiUlB9Oq4gwVs8WNF4r8vYv6zg6Fqnpe9hSoM=,tag:WYjtPdD2Lny9Kc+dWVupjw==,type:str] + users: + console: ENC[AES256_GCM,data:v1QY7h6vMmzYq/PTmnLg3M5Iwc/Ljr/1M3nJGuUJMfPN2ATY+GIMUyhYu+0=,iv:32c4YrZfTiu7sv/c9tDItmAE8G1lvc1sm6JD1GRkYr0=,tag:kj1a32+bMUsRdusadoXDvg==,type:str] + infra: ENC[AES256_GCM,data:WdugcvTJEy3/kxjCMeN16u96XM+59sOBF/z88p9f0lPVzi78ZmIZPjf835M=,iv:6G4rry8fCjhB+MF5B3TVf1w0LDSA6farVwyxAp8nKQE=,tag:+mCSrVmo7zuj782swmRFPw==,type:str] + app: ENC[AES256_GCM,data:5A17rXYUBPkFRpcHTOBCGmm9j6uMHtvKkLTpHsYZeRJkoSrA4k9IhVpjNA0=,iv:kJzowaheVckLUKae6xXeIVi+Jc+Mhee/jjmS91im9dc=,tag:f1uXU6JRD4mxAYMlFTWoew==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -117,11 +117,11 @@ postgresql: gitea: ENC[AES256_GCM,data:l+pBCzyQa3000SE9z1R4htD0V0ONsBtKy92dfgsVYsZ3XlEyVJDIBOsugwM=,iv:5t/oHW1vFAmV/s2Ze/cV9Vuqo96Qu6QvZeRbio7VX2s=,tag:4zeQaXiXIzBpy+tXsxmN7Q==,type:str] immich: ENC[AES256_GCM,data:11jvxTKA/RL0DGL6y2/X092hnDohj6yTrYGK4IVojqBd1gCOBnDvUjgmx14=,iv:oBfHxsx9nxhyKY/WOuWfybxEX2bf+lHEtsaifFRS9lg=,tag:tAfkBdgQ8ZEkLIFcDICKDw==,type:str] paperless: ENC[AES256_GCM,data:6VBrBbjVoam7SkZCSvoBTdrfkUoDghdGTiBmFLul04X/okXOHeC5zusJffY=,iv:iZumcJ3TWwZD77FzYx8THwCqC+EbnXUBrEKuPh3zgV8=,tag:u2m8SppAdxZ/duNdpuS3oQ==,type:str] - affine: ENC[AES256_GCM,data:XPXrcszsV06YqCJZ7CDqc4rCwqqNlbtLCFYfLAQ8jamLtft8L2UVrMA4WZo=,iv:vrWdBeckxB9tmEE628j4jhU+hSpE6TXYMGt0hh1Cg84=,tag:hlWwWUGht8NqWTZREMsa1Q==,type:str] nextcloud: ENC[AES256_GCM,data:ROsximNuWYMTZktmLJPx7W1Qol/uT+APgwoCtFO/6ZYYc3KxKvlk344eqEc=,iv:4d+MrfIHjJKAcwhvZ3g4go66uZcieuL7lngKErJd+fg=,tag:QbWOtxeCbiu62GyrE2atXg==,type:str] sure: ENC[AES256_GCM,data:FULJ2gjJ2gZC3s324itW+CjGRBHIP9RnOqw5TT1UaiUhb7UHAPm1na+LsZk=,iv:c0GnVZkxprJUzPPq3TCQaZvAes9QQuvDXqgVLLaiQIg=,tag:uDxy/Lkd2hNK4AWwMNMslw==,type:str] outline: ENC[AES256_GCM,data:0eJFCfHgS4olWXHOnUPixs4Dl5PoxJa+2H1mk3YgC4GUv+nX8gTPmzKTMVM=,iv:HGxX7aLODUc2nVLC8YXdet9qphU/wxMapWhk93YmHDM=,tag:6/2uckW/a24LgVFRH6E85Q==,type:str] memos: ENC[AES256_GCM,data:BaNTnZFZH4oMTnDkgrf2/fMTkO43rc9bSsgnDWrHJ/Z6+MZdMitpIMkrMN4=,iv:JsG3B0YweZYO96MW9NmoSsIG++gFq+jRUsozwJtWl3M=,tag:orLTUiW0inOJdD4YSp4baQ==,type:str] + mealie: ENC[AES256_GCM,data:XbYHQv6HBWCXUBRmcbQZN5Y1Gw+V+Wra28Mp+m+ZGl8VRXsKMu5507vGHQM=,iv:zp4ZarqG9KshiBV8F8UksfI2QXmXHwqPw631r/5bYy8=,tag:jvsvXrUsCvpQGIbLV9jTYQ==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -139,8 +139,9 @@ ldap: user: ENC[AES256_GCM,data:DT9h51o6z70A4QeoKvCbUo98JFxILgB5tkHeX60CZuCl0GHTa//mRpIUEvo=,iv:Pa9iqXeKDHHBz524zeRImYEkXmY50s7Z+Z4KYt3sNT4=,tag:UTS5VIqbRA66UVDrDXwuPQ==,type:str] authelia: ENC[AES256_GCM,data:G8ZGsLKqEmMzQ5NMAgirF5BQraHNqixtI6dyyaeNhTdXebjJZML52xL36p4=,iv:ZtHAsFYmrQxr+qoQLPW/eme0+nsT148KRsXmW/LNLlU=,tag:Pvjs/eylkgxJpmGBsRmjcw==,type:str] grafana: ENC[AES256_GCM,data:vWmU3ZKcolETWAY74C3OMD8gMXDeYk+DqssACL0xefIPi5IkbrhYWmnWAnA=,iv:wcRms3Zp8kPM4USRPVa0UHpCTK36SWhK9C8yHSWu2Cs=,tag:gU5S/6fdMZVd/ih3Yd5uJA==,type:str] - il: ENC[AES256_GCM,data:/CyMeo1+rIUAYiB25nI0,iv:jsyiiRN5z9GqcUnTZ0CZo4s+umTc2zeY2FPp+tVOC9o=,tag:cwOHcqMysCxX57w3a+Pzpg==,type:str] - morsalin: ENC[AES256_GCM,data:YryNch8hF6rx,iv:bNIBur3Jcib8BvKjJ0MejpemsurYTP8rCxo6b2R5yEo=,tag:9dIIgqEPtbeixtgJ1OtMnQ==,type:str] + users: + il: ENC[AES256_GCM,data:9b9OU4Nt60/XdUrwnhJh,iv:7ZqOYqM2aibgcFRxw1ZvOdBJMZUfOVbNa+2sMzrXAns=,tag:x87IG/HETASiC9/HwyW1Ig==,type:str] + morsalin: ENC[AES256_GCM,data:rERjBNLZXSKT,iv:VuD8ZGfrd0wQ/5TCgLhLN459ZHPepcZyv8Kdl7TsMc4=,tag:Xp/fjve1xn0Wjk7ExShCLw==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -160,9 +161,9 @@ loki: # #ENC[AES256_GCM,data:yHgd8KBDPYeMjKzUOimdOA==,iv:ozyRymtPEII2luBads2GHyLf4Vdhf4EXNaF82SNmQm4=,tag:1mOJAmR5kVRBjm0SbQkqlw==,type:comment] grafana: - user: - id: ENC[AES256_GCM,data:32LNqOb8pzi0/18=,iv:YJrYaQRE+veDhaEUefGWLbNJo1toHGmEOpqrNV3IDfg=,tag:ENryvH9ec3EbDhBB2bdHug==,type:str] - password: ENC[AES256_GCM,data:MFw7TAvzpL0EW2o+w6hF5qrLYVkn0HsQOXqBk9AQq7GxK4uAEDCi/YL3goI=,iv:HSpJL2CZKXHQYhOJLy6HzWARootl8Y1yMGhaMT0XRfM=,tag:t0VHDYHCZJwDcR1VcRFdCw==,type:str] + admin: + id: ENC[AES256_GCM,data:qwz/L8jQXvZKlic=,iv:s0PL4GwTvr/SfzuFgZESADnepagt+QKwt8XmqsMLr0c=,tag:2ozh3jpPFuqXJAx/DpgF6A==,type:str] + password: ENC[AES256_GCM,data:BDTWzGBsyr6dw3tQSCn5tFDk0IZxNc/Y6LYX6ytn1EpLhBr2CW3YE5Dsk7g=,iv:cbYmvG3EZO5ytYtoIAKpewdE81NMqP4YXrdsj7n87SA=,tag:wuFamPPldQYJAKeX7zLFnA==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -184,18 +185,25 @@ vaultwarden: admin: password: ENC[AES256_GCM,data:FzD80H5lPNyjTRGd/IcIGir35KLF8gN3qmTzEQrlfutHh08mk0Vh9X8irHA=,iv:cfn5432g2MvkuBJLB2zDJmU0sLgMefVvpy0bP/4/oPY=,tag:7MHEv8XswdQZNTARvxs55g==,type:str] hash: ENC[AES256_GCM,data:Ae7bhnL5pxfNV3+r/PCDIpxx0tHcNiB/s4sm7OoxVpp+tcEpXZ9vsv38sZjx+Z8t2vlmiYHc72PEdapcAihleuLMBPM1dAVn8LqzzWtVhfmCOoUZI8Z5L3VL8eyyz6npiw==,iv:c2MEIbrnVsR+bUx3zLLTnKSElFTH7JRl23HmmTlWEBg=,tag:mnMrER9pQEKAdloYMeRa3g==,type:str] - il: - password: ENC[AES256_GCM,data:mhLb55ENatpE59Rbzk2Uq6iBKc0Jj/9x4fOANJOHR0WvSxTQRKJ94cCq9Ykp/chWbdgYYPrwU5oD0Yo17zqb,iv:6bzKq5WaKhuOsQ8zSSH9ZrQYbDPB1nv/bFoQou1ycL4=,tag:bSmuUodl+/9nzxZ7YBoezA==,type:str] - morsalin: - password: ENC[AES256_GCM,data:ibg5/MfLH7pSY2pEmjM=,iv:+aV5muP/9BYoKwTGQxKEL+IGY9P+O3GVKGgSuTzT+U0=,tag:rgqcmJvd1RtvWJ91PCxYIg==,type:str] + users: + il: + id: ENC[AES256_GCM,data:V4kWLbzhIM7ONCt7PtxB56oilw==,iv:b9mkOmmOmyvlYI11ASRh4GQl3NxnvtoC9jAx5DuRSwM=,tag:Eo5a5UnvGVXY1jofbMgqQQ==,type:str] + password: ENC[AES256_GCM,data:Ay0aPgrP75z3n/xyELAPhi4Ov4a62M3lhtpByDkc++TGq+a2bmpdt0OhBfsCSjY/6ads1EUhqQYscTE86Eln,iv://EEyABWzFn9kiKxjz92zKbzUtbnAPf9oNxf9s0nlLo=,tag:cZpqyTXJuO2yQByHMTWwtA==,type:str] + morsalin: + id: ENC[AES256_GCM,data:fXTVMR2iUa1lfyLjG3UI1Xuys80ylFEQ+A==,iv:KqRhjFULUc9BjGEepBu9I3aZparaxRhLos8Ztz3jPyE=,tag:1E8DgiXt810B7UYZLE+0yw==,type:str] + password: ENC[AES256_GCM,data:e8H0wYtCbVEDDXctbJQ=,iv:Bewk1rEsDPD5NVzZfEM2QfzNttwklI6KL7kFgFU3qvY=,tag:0U8Vg39ZD05AWMp6hQkkzA==,type:str] + eunkyoung: + id: ENC[AES256_GCM,data:OubNz5ueQU/KksljCvS/X29rhDjvipkgzik=,iv:mULicCrst9ciC7lAJrYIPV8Kx4Z/JAEbupO/Kti5DoA=,tag:0U/WQnZ8ZCL5yfkbd7Y6oA==,type:str] + password: ENC[AES256_GCM,data:M3MqxQTZdd+SS/nd,iv:KEBZ8eBAwVm0mTwDf3bJRWwtUN/M+an6BMV9N34pXq4=,tag:Qxxca3kq5NH6YbJWK002Gg==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # #ENC[AES256_GCM,data:HAN9BBEl1CW11LAf3x8=,iv:hwqCErtmPqGJ+y86D9MoxJwixvbcJONlyT900Y5DOug=,tag:kCxsG353ltbOCKLe85adCg==,type:comment] gitea: - il: - password: ENC[AES256_GCM,data:Bs5/t5mNbSv5+ek9gNHZp5qqxitM4Kq0Xgh2JV6LiFw9lZJSOVT4JPLRP5M=,iv:z50+naWOTVL8lEgBgm51j6hLjS8ve2UcRRKukvtykM0=,tag:TF9rPxjoLe/vAyvl23PiCg==,type:str] - token: ENC[AES256_GCM,data:CiYEXEKLLRDp++iga5YmhyPB2bSvqhhDgSOhmiulp9n9SsBVQ3s5MQ==,iv:+oetgEH/IORz74Xoz5zgDjD3BLyledZTqlYlCWaDrRk=,tag:S9O09YEMuSexuNW3ojEw5Q==,type:str] + admin: + id: ENC[AES256_GCM,data:PtE=,iv:44l071j9ZLt3T2BbpAho70soiHxzBqp4KeITxrjdem8=,tag:yQt8jyEadk/GnrxnCPVKdA==,type:str] + password: ENC[AES256_GCM,data:EazzQF9D/s5TtYKBApzLQT3S7a0itGm7wGtHRMi6J0C3I/gBNhpgeF6dk8k=,iv:SGSiAlqOO7LvTlyVmOHU13Vq7OANo3eAeBCwioFpi3Y=,tag:4Vz8FRHEv9G+at/zZq4YMg==,type:str] + token: ENC[AES256_GCM,data:z12Oq4OFCCjCXn85cVSfSzxWt7b0ytuA6TAn3Xf5CIhvKuoQD43WnA==,iv:BP8WDOAXkuA9fMjfLR7BTyTqjEbjSmwgSrJfqCutwmQ=,tag:s7ys3V+QnU/Nmkccm3rcDw==,type:str] oidc: secret: ENC[AES256_GCM,data:qjQosi83oaK47wX4VtDktDFlIU3FQgmDpwyiSgJNPDq2Dtc/QEKzdv5/ZpQ=,iv:aUJx5a6Wj7lhaD19aPiZWIE/MWKUgH0muxtTSkwfg8M=,tag:DttjXee7ntxRAbqbFq6qeQ==,type:str] hash: ENC[AES256_GCM,data:iVls8UzdP6JTfRosET6nsk3RcEtFQ8ak6GtPuBjcqAv3lyA9oSLwvvC8MvGaT1aHCB+5y/lspuloHEaAcVGNuzGCIVLg9Adut4a7LYDTHNOZnJIKLj8ldL1ytD5XVdbeBhZGCqoTs088oO0HVCQiDzpJL5NdmM3Ru6egyYBCLLUOlPA=,iv:er63GEC+kxNDscvspvvLiq17VSg0GeZ2w3jmGojF9PM=,tag:F1pDpaQrVdomIlZ8psVpAw==,type:str] @@ -204,8 +212,9 @@ gitea: # #ENC[AES256_GCM,data:HvMeGuC8JwK50pO1E/nm,iv:5NFTyjesMX0ZnBpH+hEv8jQ0G2NvrDtT23CUyLbQcUo=,tag:qWmPvQpADTeD+W8nWXRQvA==,type:comment] immich: - il: - password: ENC[AES256_GCM,data:PbDFc4m7rNPPN1mCjcvhbKwf/EbiJxdvO/iMspf9jMuCqQyGv7h6VrZqk98=,iv:hlMAp5wXXkFO9+ekq3A2/ioF/EX8Uau0puhb4TAHkRQ=,tag:pfS0W2JNaFNMpBlKgZ3Pjw==,type:str] + admin: + id: ENC[AES256_GCM,data:DclWIxkt7yZl/8EXSWpOKhVR8g==,iv:ekuWHM9tzQEGRj5Dz6txgh5H89dWVRojjICd7td3QSo=,tag:ZSBvlXtGrNn9T82Agn/vtg==,type:str] + password: ENC[AES256_GCM,data:AcaAeRVzeKCp9Hhx202DMuVTOmoPdXrUqpgwg6Nj/F7b9rCnQRwpsGGmuw0=,iv:z/PhfW75B08K+zbxBrEtBKFYWxTdRdgaicSYHe6yyJ0=,tag:8K+9QHjUF6lHhwnNDT9L8A==,type:str] oidc: secret: ENC[AES256_GCM,data:9ENcp2Ns21OLXDY05zRoSdP+93EiwSH8MGzZZpxK7sToe4QLUXWt9w6xQIE=,iv:Q/VcnArZHs/J2YLRVFXt3Mp+LYfuq4PD/trqO8Simig=,tag:BY/mOyZpYmoAc7NrASlmSA==,type:str] hash: ENC[AES256_GCM,data:mrML2CWFFtGjq8wfWipVpv+pjJRSHe74VGC7Eoa6588R5C/sCnC3W5aI+dsRCZN3LRCjHAkOJJgjeYrwcXYdKRauXsAYR51dNSsHqqSN3WebLxapRDwcYu5e4j5RN1aPHsysr7GaQ4hhe5rKW4ORCGC3Cp3Ob+LChPy4bdCAZG3bN9k=,iv:Q4hmqhq+dvIr7DxCpcqP4E0NKyFZkOeTnDpGctmCxXM=,tag:/gji6AFkHnYwkQf3FSQUxA==,type:str] @@ -215,29 +224,20 @@ immich: #ENC[AES256_GCM,data:McPUAbIUvtC1gdPaxTgAxAMCMWcLfg==,iv:Tp6idRf7he3sYzo8LW596C905JAaoTIhIoDUzSyRT0k=,tag:4mZQ0Swu1X9uuwjsRNhr2A==,type:comment] paperless: session_secret: ENC[AES256_GCM,data:siwCs2noeVpg9DCEZybnmo/oz11BdrHSTnHciMOu/6g=,iv:XVjhu10TIujIdUopN9+TVVqRade9EvItDWxym6YXnZs=,tag:TxLYm+4Bo7IMaTQBtMg9pQ==,type:str] - il: - password: ENC[AES256_GCM,data:9bJHf+chTg1rppgNVafNgEuvwQ69Gx+w5d65hu68q9XeeaVb2pO9HE4BOgg=,iv:1kaXBg/iOoIZxDjEVEdaMJLDtp6zQjep3vxLmIgQN5o=,tag:+MgX8Oa3tmhjx6u9aHkDfQ==,type:str] + admin: + id: ENC[AES256_GCM,data:8uo=,iv:f/qtpDb1Xe0M6Gd2VFP2vXULCd7WOn7TCxLlaK1dDbA=,tag:eNRMPJtLlA3Vjj79bSFxuA==,type:str] + password: ENC[AES256_GCM,data:TAA+TRTYRUSw47nE7oPDQmOtCBZpf2WyxUIIzftWq16wQhseODhH0R74X+Q=,iv:V0tzSvW5iYzEUgcxempmyD9ermVAqg673liiysT4jJ4=,tag:GuxtZ7EvgwvlFWwg+C6Jsg==,type:str] oidc: secret: ENC[AES256_GCM,data:wjRDVCJsINM4z5946a6uZD+6bhN5BChLMdRzgMEJFGRGFNcXd7A1p2Iqn4I=,iv:Y4QDA09L8ULKr4hhvoiduzCD8Hifo1gAnpzjCr8e520=,tag:R0RvGxYnXo3zwykXJykRug==,type:str] hash: ENC[AES256_GCM,data:pali6WwPNhJA+6QL4O+tKv42PnpGqmojb8JQUZLqxGizv1bJSCgdUN8upCy5Ke0DYZs5P+JY5vh23xfMZFnHduGxGwOuPX6J5lYgvJRV58LqS3/+yIBBprTJyro3MwsurTTEWesgKMr8/2H9lirhaLjWUOSPxAmQ6e4wPNpHycDVyj4=,iv:cg2trI7t1MfIcMo1/M+IY6JEl2msDoKRGgAx/Y5nyGk=,tag:gnOq7sBq9z5zrRY0yhIabg==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # -#ENC[AES256_GCM,data:k55osvepVeB1RC5hZ4IF,iv:AlhfmWwn/DiSESWc+ULJSOLUhnrKAIfWr7MeiwV8qc8=,tag:hOgptwUcY6nVxPIhu+DYgw==,type:comment] -affine: - secret_key: ENC[AES256_GCM,data:LLX78DpYnha1JWhgw0sHLzIVq/oIzvT+nB7zgli4mroGbnt7WZaXCx34zKkYRwYj/+0L4IFFVdkzKtK5DO84SgFkS2Bk2iNdCMqIx80CpyiD8IWAcyRu5d6hh82PlgyxU80T/4nbLbIn0GLubPTTeUX8GC3VxRU=,iv:DnmvbhlygSHes0jAkIm4+WXMUQLzr4R4dNa33rO67v8=,tag:+2wlh+/ekiTyShWM4XBbUw==,type:str] - il: - password: ENC[AES256_GCM,data:4zxiQAzXTR+fraRjYT657BIwSqrih3lMPFFSibQdardRMjskAbuRYIQA6mo=,iv:ub3giRG9vCFSuwRXDazYTqWbjENzQUWR36290Kruj1o=,tag:C2Ixd2eTEgzBvUNCNBtJuA==,type:str] - oidc: - secret: ENC[AES256_GCM,data:eRDBrqLZR7MFLlsUwk7Wg7FzxDov7vJLIWQRuKq7vrXbPSJkMcy9jfG2rL4=,iv:UaSoi7gODXgjzihJIDVIdDHJcSAZNV8UKfGeM6YzxqI=,tag:cOUDblcMStP8E4fp+s1WRQ==,type:str] - hash: ENC[AES256_GCM,data:jE1CvFo+mjb/Xc3Ft5ky7on03vcnv79cw/5g/xaldXsv94VRrIjmfGMgHAj07r8j5mDpP34A5bYO1PSe9DYrwRcsXa9OUQuzm/8avFy9wVZDhBUUAGR+jiW1BP9hc6nmSpPVPtle+3sbqOB0ZMjXWwlcAcuknOtuhH1mzwmaDP9yf+M=,iv:CSSaXY/6MpHBMhPLUWPkabIeJ9zpZkcVjiEhxVF0zJM=,tag:f72ekkjJs7Qmh1K9wC8L9w==,type:str] -#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] -# -# #ENC[AES256_GCM,data:PZS7EbvMHqHGorNUGAWj4dk1,iv:vOE+djRAvBTMM51kHi6kG5Arw3uPXlJt1d/BpcEaD0c=,tag:AuoCHLQz42CYvVVdKFWu1Q==,type:comment] nextcloud: - admin-local: - password: ENC[AES256_GCM,data:mIwF5A09oqYbdK3bOKid9A896Q5J5Q6Ax+vDNqEJFGNdzd/mJ4oQS6rva+s=,iv:QroUMST2wnEJzk6DySe9tPZaWuqdxzJZ0+oi6mW6x00=,tag:3UTzjupK7+omrI3Hvyr8bA==,type:str] + admin: + id: ENC[AES256_GCM,data:uSov7SN1yB1PhTc=,iv:5yxabQcY/XsBhn531xZg3XYsnxl3K59WuN7Vz9M4XG0=,tag:BBnfZPppw+SDGsV8dK2F+Q==,type:str] + password: ENC[AES256_GCM,data:j/wFr5EV00WegWxIGj1k3U13sQjf1GgUofGbh9+i916uudfdklhcurujhiY=,iv:aHEKjLvS5lnayRX/NJ98DxQ40mfqssidubDaP2RVb3k=,tag:zQRE9AsiAlXe58r4VFJiaQ==,type:str] oidc: secret: ENC[AES256_GCM,data:Sr4KkKkYdkU0UWdpfUF7PyiGoerjBiw+sOFcENyLxw0FRXGG0Y8gv5uGb4Q=,iv:LbGsNM3+iY7bWFQe88TepVKUdiRQWZ+K7Ubn6ze6lV4=,tag:SbcfIAMW9ZprgahOFU4IQQ==,type:str] hash: ENC[AES256_GCM,data:CkstbIYQmi72QhsbJZN0lQedgCn7TmGpYcYj0n+NvJIoTlol8G9N/88cwGbVoGK9nEISv54FL94cEJFppnMIuj0BHrhasrZsyI2/Lj52YLWdwNJWNQ+iYt+Ifp/1kI0zqmdoajzZ5DS2w/1evCBC1+JdfTRlpVXmSsHUIPIHelBRj90=,iv:vwvT5TTkF4woxXOvrRRqmrdLXf19s47NIDtdT+zLp0U=,tag:KC0MS0DTH6j3zIHOjCFOSA==,type:str] @@ -271,9 +271,18 @@ memos: #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # +#ENC[AES256_GCM,data:ATLTbBi1RsBr2ldHDDH4,iv:RFbPz/46BIGYv9Wo5Jxw5Swi4Xo2JgIEI4IsO4rCQoQ=,tag:i7/2ItQSFurrmdNaBWwWAA==,type:comment] +mealie: + oidc: + secret: ENC[AES256_GCM,data:ldsfwWnexNNM4r+aP2h+woQROwbp9MpxAz8E/o1Q53GxcEwtixqvP6QZGoo=,iv:6M/lfuDU3IOd0nxLi2l02OD5FopErhSCp6AhxBGTI1k=,tag:3w0p+wi9T1PqNmgoM59gug==,type:str] + hash: ENC[AES256_GCM,data:YmziHxsFPJ93uMNG6/EUiPEfMvvAZM8/S5854GJ/fPPTMvXj9cM2sovkSrcNd7H0ac5KDJIcJoD9SC5lWKCAbjKzxHvI6CdI63SM8bExrDL2PVSGAf29VLU/cz8rQtZS39GWz2v5GWlpk67Emy0T115vruIGfDlan8zgQKK7YIWyVMs=,iv:xmnBYjBIWq6P2g5DyMh+wYskmJRcVDREGL/yYhJHyRs=,tag:qMWXHiLmAy5UvWFvv3Ct8g==,type:str] +#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] +# +# #ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment] switch: - password: ENC[AES256_GCM,data:qu0f9L7A0eFq/UCpaRs=,iv:W8LLOp3MSfd/+EfNEZNf91K8GgI5eUfVPoWTRES2C0Y=,tag:Q5FlAOfwqwJwPvd7k6i+0g==,type:str] + admin: + password: ENC[AES256_GCM,data:cwBJRIO3eUgezyN60dU=,iv:Ab+EpURW+MYT7RTcPHi4FvQiv4m6TDhAb1/xexkIHCE=,tag:JEWZ3cqoAfAPfwXz7KmINQ==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -281,8 +290,9 @@ switch: dsm: key: ENC[AES256_GCM,data:jxQRPNuioZbznadJa7sdhbntABsi+nc6Y5TJsyMXwKRvVUbQw8p3snp75d9WVmOfq4uj8wBGen0noa/6xtWo4ZiHsIg1Svh/5xAN37zueB6NmK/z+BW2y8HaIkNhjcinusnT2SWE2ahguizMNM90FpXgmTt9Zv7wVwRkifDLWpH+rs5zmyMWPtpT2RN3+k7tVh8XWVevek/DxG4h+jtoYhTVzndMC5oe/A60MHBJCj6kaksGlX3vIDbRo5A0FEKX8uWI6k6U2osFc4SHIvpljYmPagj3EwPaTc3mbl8IJsXFVH4=,iv:7O5oQa1fELjIt6f3nQ+fWDFirAexgioS36/Nozh656o=,tag:t1RHmY8QC6mE9PVPJg426Q==,type:str] crt: ENC[AES256_GCM,data:N32+8CItiwbW8drpnL65weS35SY8uP8vcw9wBDFOxGFQjs6EyEwsMUVh1ndMpmDSUKWz7c7ji0HisC710RqnZw9l2NMg6R57DeXR/WsLW4LiAT7sFuDBcqorTB+6Nk0K9wLLdnsKpUQgl8oV9ffnWXZdfI5skKCDMNB40WJAYY/3wx9iT3wDQWaunq/1NT0SZHrtXXTj1JYiLbfDaTwTvkuqqUv1cXvOjonL2Cp4aNT/zjwK6c/TRbKdhIs/7tnUUE/lJx74eARfrMQy/3y6h2xK9pgruASuJxMw9Tyaj6Ya0FMNMIezO3GPQbFCE/IRfmCE6vX2RXUjVqX8wd/d4m4KNuMsN3hbwC3vQ6r+DcYdaTUfGIu96xfKSiSax/ZPQ4JhTyqthkGgpE0Ob67aVELrZkyhc/kvvSxafVSvP63C/nHPQnXMgitdRddYFq7487Vbq03frxiyID1WkqeUavc+Tn3dyA8c7aqVvbW6vZCAwUzbKD0nwPuBUtKZQpJKxHvtx2390PW7a214zkPLcw5LH3FnNacopiNfs6dNEeW/UsGysYkoBRXfrAPMX4UL53TvUTrpuCBhthJ4rgqjhgTfq9GtomP7piuf/tnMAO33qSHPM3w2ocLWeLyOiqYESFmXoBAsiu33LVzuVryFVw75pgrGAsc2wwKhcu4JC0cNgUE44ENIIjffVQX/mcvPvHxuI05vtzM0N8kOFPwG7iAVrO1eUEpwUjZwUwkk8QRsuIhZ6W7aYx7TLdPDpjmy6XCYGNHZgHg6jU7v18KkPep1YarAnsVYFzHQwfZIMOzW7DHlHq38NJW8lWq7zkg+kY6JDVy0nttlAUSskWNeCHdI5Xy3Rz3t7V6t1+UxkSy3roQCZOYjiaknwrXSctsgqucKaJCq4w0Tty36kaIXwOQYVm9JqDIztOmer4fW8Es7OEbLwA==,iv:26fGb288JEbtA0UcAmiYcN+UunrUOw2S6fKjGMetgwE=,tag:GmUJb9LJ186HNOF4AhfmLg==,type:str] - il: - password: ENC[AES256_GCM,data:6LiNif7aIGgGBBdVYpl4K8L8ev6EPru5x6HvX2yJ0oiDQb6G9AI+ZsaKamc=,iv:Y+eijt1PmNZR9c9uCzBnzJT9BDc3w/P4VzsbvrPRU64=,tag:2KKRtTSYzrJKy9VX/2J5eQ==,type:str] + admin: + id: ENC[AES256_GCM,data:/Js=,iv:GyEcag6KxEHy6xApfzEo+dZF9NOtkOoYaJZEzPa8ing=,tag:W/4zaDxdw42Ap4DVE9ogAQ==,type:str] + password: ENC[AES256_GCM,data:/rgn1Eogw3F2EqArMJXL8Ri4y1W04hGzxy4w1Ct5/ysBjjHGgRbxd9tXSf0=,iv:PISJ0a113rlEL0z0yo+nBYNQNU6Ht0PgFWZzooxIgi0=,tag:UZl0EiVcaVPq1v0MtNCXFQ==,type:str] oidc: secret: ENC[AES256_GCM,data:IXJNy9PmMPn9uowSMMrNPYqaW5PZ13JTeu1t7eNlapxmnV3EAl3I3xbMK5A=,iv:UcG2E6zgoKkPfUiHr0A+gvnApCkBJG2GROfacwD634Q=,tag:nSFn9yaCadJGlGy/aCvc2A==,type:str] hash: ENC[AES256_GCM,data:Y2/FNYbID0trRS5UpP2MX6UcLWxlRoXu9VLeU9DU9whEwNLbf4gYrxI7Q7etQe6HidBuoIJNxKajf3t0fI0ptbXnJaxzMh/v/CsiDg2p568o3KIyfB1PruGRKUrTuJneREUHg5jRXMYCwcRUQ4C5WgAyjd2spGlTQO0OLEqh0wkTtvs=,iv:YTYoaBy3QwlSrmx/K5woiXAHrh0RkawF+uQ4rQuP7WU=,tag:Ej6ZjTFvKjw6BrXbd0lf0g==,type:str] @@ -291,8 +301,7 @@ dsm: # sops: age: - - recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y - enc: | + - enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0TVVta2V6UHB4V3NmV0tR V0hmbUw1bUpSSk4zQXZ4MHlaVzdJU0VSWm1VCnZaMUg1cjJyOEFyOWxPNm9lWkVm @@ -300,7 +309,8 @@ sops: UmliaFNxVTBqRkI1QWJpWGpTRWxETW8KEY/8AfU73UOzCGhny1cNnd5dCNv7bHXt k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-05-16T11:08:23Z" - mac: ENC[AES256_GCM,data:FprpwGfSw+KObEthT41tD8qgawQPpRlHw0ZxcyH74Nzk+47AbkDR3dWUPmZGPTePcQM4WI3e1IzE0ptJA7Hpcsnq/wHdo+uTW8nozYxzDU7JhjG2Xn6CPov2mV3o0SCegiCpfvE4A3bXiBOPapUIIs+ySBUMmyyW6m5ravElxsA=,iv:wt/3nFmVfodxActWBNvli2M2LLQZG8bkuagA/Fc6iuk=,tag:z1QWaMrGN/8kBrmZ8mPJUA==,type:str] + recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y + lastmodified: "2026-06-12T13:44:23Z" + mac: ENC[AES256_GCM,data:FMOY7qMDSCzvOUL44zmwLwUVAXfYkat1JbiNeBVQFuUrC4lAwuukxW9MCPs5plj84LOAUMVgMofyudFJh5VKTR6Y0EnB5ft0NnbGgwMwzQnQn7d9Mcg9xPca3ccSbSGNx0UrC+8lTjXuzhoawMQ5ZXZ3ZK0k7JrFQ2El7UUwxEA=,iv:+oj0bvsDo8gidUn6m0FOeWsJOxIXPHhzvtiIbQ1VgcY=,tag:8xLcthizKqWjqg2eQvJlRA==,type:str] unencrypted_suffix: _unencrypted - version: 3.12.1 + version: 3.13.1 diff --git a/config/services/containers/app/mealie/mealie.container.j2 b/config/services/containers/app/mealie/mealie.container.j2 new file mode 100644 index 0000000..d8ed020 --- /dev/null +++ b/config/services/containers/app/mealie/mealie.container.j2 @@ -0,0 +1,49 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Mealie + +[Container] +Image=ghcr.io/mealie-recipes/mealie:{{ version['containers']['mealie'] }} +ContainerName=mealie +HostName=mealie +PublishPort={{ services['mealie']['ports']['http'] }}:9000/tcp + +# Volumes +Volume=%h/data/containers/mealie/data:/app/data:rw +Volume=%h/containers/mealie/ssl:/etc/ssl/mealie:ro + +# General +Environment="TZ={{ timezone }}" +Environment="BASE_URL=https://{{ services['mealie']['domain']['public'] }}.{{ domain['public'] }}" +Environment="ALLOW_PASSWORD_LOGIN=false" + +# Database (PostgreSQL) +Environment="DB_ENGINE=postgres" +Environment="POSTGRES_SERVER={{ services['postgresql']['domain'] }}.{{ domain['internal'] }}" +Environment="POSTGRES_PORT={{ services['postgresql']['ports']['tcp'] }}" +Environment="POSTGRES_USER=mealie" +Environment="POSTGRES_DB=mealie_db" +Environment="PGSSLMODE=verify-full" +Environment="PGSSLROOTCERT=/etc/ssl/mealie/{{ root_cert_filename }}" +Secret=MEALIE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD + +# OIDC +Environment="OIDC_AUTH_ENABLED=true" +Environment="OIDC_SIGNUP_ENABLED=true" +Environment="OIDC_CONFIGURATION_URL=https://{{ services['authelia']['domain'] }}.{{ domain['public'] }}/.well-known/openid-configuration" +Environment="OIDC_CLIENT_ID=mealie" +Environment="OIDC_AUTO_REDIRECT=false" +Environment="OIDC_ADMIN_GROUP=admins" +Environment="OIDC_USER_GROUP=users" +Secret=MEALIE_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/app/nextcloud/ini/opcache.ini b/config/services/containers/app/nextcloud/ini/opcache.ini index 58dfc68..6b478ef 100644 --- a/config/services/containers/app/nextcloud/ini/opcache.ini +++ b/config/services/containers/app/nextcloud/ini/opcache.ini @@ -11,4 +11,4 @@ opcache.revalidate_freq=60 opcache.fast_shutdown=1 ; APCu CLI activate -apc.enable_cli=1 \ No newline at end of file +apc.enable_cli=1 diff --git a/config/services/containers/app/nextcloud/ini/upload.ini b/config/services/containers/app/nextcloud/ini/upload.ini index d4c2130..dd2cdf3 100644 --- a/config/services/containers/app/nextcloud/ini/upload.ini +++ b/config/services/containers/app/nextcloud/ini/upload.ini @@ -3,4 +3,4 @@ upload_max_filesize=16G post_max_size=16G memory_limit=1024M max_execution_time=3600 -max_input_time=3600 \ No newline at end of file +max_input_time=3600 diff --git a/config/services/containers/app/outline/outline.container.j2 b/config/services/containers/app/outline/outline.container.j2 index 56b1ceb..1b76e1b 100644 --- a/config/services/containers/app/outline/outline.container.j2 +++ b/config/services/containers/app/outline/outline.container.j2 @@ -53,6 +53,7 @@ Environment="OIDC_USERINFO_URI=https://{{ services['authelia']['domain'] }}.{{ d Environment="OIDC_USERNAME_CLAIM=preferred_username" Environment="OIDC_DISPLAY_NAME=Authelia" Environment="OIDC_SCOPES=openid offline_access profile email" +Environment="OIDC_DISABLE_REDIRECT=true" Secret=OUTLINE_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET # Mail diff --git a/config/services/containers/auth/authelia/authelia.container.j2 b/config/services/containers/auth/authelia/authelia.container.j2 index 6db7cda..9673780 100644 --- a/config/services/containers/auth/authelia/authelia.container.j2 +++ b/config/services/containers/auth/authelia/authelia.container.j2 @@ -54,8 +54,8 @@ Exec=--config /config/authelia.yaml [Service] # Wait for dependency -# They run as rootless podman container, so their port is not opened until they are normaly running -# Check their ports with nc command +# They run as rootless podman container, so their port is not opened until they are normally running +# Check their ports with nc command ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} # services['ldap']['ports']['ldaps'] is 6360, but nftables works on 636 the original port ExecStartPre=/usr/bin/nc -zv {{ services['ldap']['domain'] }}.{{ domain['internal'] }} 636 diff --git a/config/services/containers/auth/authelia/config/authelia.yaml.j2 b/config/services/containers/auth/authelia/config/authelia.yaml.j2 index f28c7d0..3e7424d 100644 --- a/config/services/containers/auth/authelia/config/authelia.yaml.j2 +++ b/config/services/containers/auth/authelia/config/authelia.yaml.j2 @@ -4,7 +4,7 @@ # certificates setting certificates_directory: '/etc/ssl/authelia/' -# them setting - light, dark, grey, auto. +# theme setting - light, dark, grey, auto. theme: 'auto' # Server configuration @@ -22,7 +22,7 @@ totp: # issure option is for 2FA app. It works as identifier. "My homelab' or 'ilnmors.internal', 'Authelia - ilnmors' issuer: '{{ domain['internal'] }}' -# Identity validation confituration +# Identity validation configuration identity_validation: reset_password: jwt_secret: '' # $AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE option is designated in container file @@ -33,7 +33,7 @@ authentication_backend: # ldaps uses 636 -> NAT automatically change port 636 in output packet -> 2636 which lldap server uses. address: 'ldaps://{{ services['ldap']['domain'] }}.{{ domain['internal'] }}' implementation: 'lldap' - # tls configruation, it uses certificates_directory's /etc/ssl/authelia/{{ root_cert_filename }} + # tls configuration, it uses certificates_directory's /etc/ssl/authelia/{{ root_cert_filename }} tls: server_name: '{{ services['ldap']['domain'] }}.{{ domain['internal'] }}' skip_verify: false @@ -59,6 +59,7 @@ access_control: # policy: 'one_factor' # subject: # - 'group:admins' + # Session provider configuration session: secret: '' # $AUTHELIA_SESSION_SECRET_FILE is designated in container file @@ -99,15 +100,20 @@ identity_providers: key: {{ secret "/run/secrets/AUTHELIA_JWKS_RS256" | mindent 10 "|" | msquote }} - algorithm: 'ES256' use: 'sig' - key: {{ secret "/run/secrets/AUTHELIA_JWKS_ES256" | mindent 10 "|" | msquote }}{% endraw %} + key: {{ secret "/run/secrets/AUTHELIA_JWKS_ES256" | mindent 10 "|" | msquote }}{% endraw %} + lifespans: + access_token: '1h' + id_token: '1h' + refresh_token: '720h' clients: # https://www.authelia.com/integration/openid-connect/clients/synology-dsm/ - client_id: 'dsm' client_name: 'dsm' + consent_mode: 'implicit' # It depends on application - # hash vaule generate: - # podman exec -it authelia sh - # authelia crypto hash generate pbkdf2 --password 'password' + # hash value generate: + # podman exec -it authelia sh + # authelia crypto hash generate pbkdf2 --password 'password' client_secret: '{{ hostvars['console']['dsm']['oidc']['hash'] }}' # If there were not client secret, public should be `true` [true | false] public: false @@ -132,6 +138,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/gitea/ - client_id: 'gitea' client_name: 'gitea' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['gitea']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -153,6 +160,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/immich/ - client_id: 'immich' client_name: 'immich' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['immich']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -176,6 +184,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/paperless/ - client_id: 'paperless' client_name: 'Paperless' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['paperless']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -195,30 +204,10 @@ identity_providers: access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' token_endpoint_auth_method: 'client_secret_post' - # https://docs.affine.pro/self-host-affine/administer/oauth-2-0 - - client_id: 'affine' - client_name: 'Affine' - client_secret: '{{ hostvars['console']['affine']['oidc']['hash'] }}' - public: false - authorization_policy: 'one_factor' - require_pkce: false - pkce_challenge_method: '' - redirect_uris: - - 'https://{{ services['affine']['domain']['public'] }}.{{ domain['public'] }}/oauth/callback' - scopes: - - 'openid' - - 'profile' - - 'email' - response_types: - - 'code' - grant_types: - - 'authorization_code' - access_token_signed_response_alg: 'none' - userinfo_signed_response_alg: 'none' - token_endpoint_auth_method: 'client_secret_post' # https://www.authelia.com/integration/openid-connect/clients/nextcloud/#openid-connect-user-backend-app - client_id: 'nextcloud' client_name: 'Nextcloud' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['nextcloud']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -241,6 +230,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/sure/ - client_id: 'sure' client_name: 'Sure' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['sure']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -263,6 +253,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/outline/ - client_id: 'outline' client_name: 'Outline' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['outline']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -286,6 +277,7 @@ identity_providers: # https://www.authelia.com/integration/openid-connect/clients/memos/ - client_id: 'memos' client_name: 'Memos' + consent_mode: 'implicit' client_secret: '{{ hostvars['console']['memos']['oidc']['hash'] }}' public: false authorization_policy: 'one_factor' @@ -304,3 +296,26 @@ identity_providers: access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' token_endpoint_auth_method: 'client_secret_post' + # https://www.authelia.com/integration/openid-connect/clients/mealie/ + - client_id: 'mealie' + client_name: 'Mealie' + consent_mode: 'implicit' + client_secret: '{{ hostvars['console']['mealie']['oidc']['hash'] }}' + public: false + authorization_policy: 'one_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - 'https://{{ services['mealie']['domain']['public'] }}.{{ domain['public'] }}/login' + scopes: + - 'openid' + - 'email' + - 'profile' + - 'groups' + response_types: + - 'code' + grant_types: + - 'authorization_code' + access_token_signed_response_alg: 'none' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_basic' diff --git a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 index 0af9f72..03f9dc3 100644 --- a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 @@ -53,12 +53,6 @@ header_up Host {http.request.header.X-Forwarded-Host} } } -{{ services['affine']['domain']['internal'] }}.{{ domain['internal'] }} { - import private_tls - reverse_proxy host.containers.internal:{{ services['affine']['ports']['http'] }} { - header_up Host {http.request.header.X-Forwarded-Host} - } -} {{ services['nextcloud']['domain']['internal'] }}.{{ domain['internal'] }} { import private_tls reverse_proxy host.containers.internal:{{ services['nextcloud']['ports']['http'] }} { @@ -89,3 +83,9 @@ header_up Host {http.request.header.X-Forwarded-Host} } } +{{ services['mealie']['domain']['internal'] }}.{{ domain['internal'] }} { + import private_tls + reverse_proxy host.containers.internal:{{ services['mealie']['ports']['http'] }} { + header_up Host {http.request.header.X-Forwarded-Host} + } +} diff --git a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 index 4896378..e9fd55f 100644 --- a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 @@ -1,7 +1,7 @@ { # CrowdSec LAPI connection crowdsec { - api_url https://{{ services['crowdsec']['domain'] }}.{{ domain['internal'] }}:{{ services['crowdsec']['ports']['https'] }} + api_url https://{{ services['crowdsec']['domain'] }}.{{ domain['internal'] }}:{{ services['crowdsec']['ports']['https'] }} api_key "{file./run/secrets/CADDY_CROWDSEC_KEY}" } } @@ -98,15 +98,6 @@ reverse_proxy https://{{ services['paperless']['domain']['internal'] }}.{{ domain['internal'] }} { header_up Host {http.reverse_proxy.upstream.host} } - } -} -{{ services['affine']['domain']['public'] }}.{{ domain['public'] }} { - import crowdsec_log - route { - crowdsec - reverse_proxy https://{{ services['affine']['domain']['internal'] }}.{{ domain['internal'] }} { - header_up Host {http.reverse_proxy.upstream.host} - } } } {{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }} { @@ -154,6 +145,15 @@ } } } +{{ services['mealie']['domain']['public'] }}.{{ domain['public'] }} { + import crowdsec_log + route { + crowdsec + reverse_proxy https://{{services['mealie']['domain']['internal'] }}.{{ domain['internal'] }} { + header_up Host {http.reverse_proxy.upstream.host} + } + } +} # Internal domain {{ node['name'] }}.{{ domain['internal'] }} { diff --git a/config/services/containers/infra/postgresql/config/pg_hba.conf.j2 b/config/services/containers/infra/postgresql/config/pg_hba.conf.j2 index 63b0495..888f3b3 100644 --- a/config/services/containers/infra/postgresql/config/pg_hba.conf.j2 +++ b/config/services/containers/infra/postgresql/config/pg_hba.conf.j2 @@ -1,28 +1,29 @@ # @authcomment@ -# TYPE DATABASE USER ADDRESS METHOD +# TYPE DATABASE USER ADDRESS METHOD + # Local host `trust` -local all all trust +local all all trust # Local monitoring connection (host - infra VM) `trust` -hostssl postgres alloy {{ hostvars['fw']['network4']['infra']['server'] }}/32 trust -hostssl postgres alloy {{ hostvars['fw']['network6']['infra']['server'] }}/128 trust -hostssl postgres alloy {{ hostvars['fw']['network4']['subnet']['lla'] }} trust -hostssl postgres alloy {{ hostvars['fw']['network6']['subnet']['lla'] }} trust +hostssl postgres alloy {{ hostvars['fw']['network4']['infra']['server'] }}/32 trust +hostssl postgres alloy {{ hostvars['fw']['network6']['infra']['server'] }}/128 trust +hostssl postgres alloy {{ hostvars['fw']['network4']['subnet']['lla'] }} trust +hostssl postgres alloy {{ hostvars['fw']['network6']['subnet']['lla'] }} trust # Local connection (in postgresql container) needs password (127.0.0.1 - container loopback) -host all all 127.0.0.1/32 scram-sha-256 -host all all ::1/128 scram-sha-256 +host all all 127.0.0.1/32 scram-sha-256 +host all all ::1/128 scram-sha-256 # Local connection (host - infra VM) needs password (169.254.1.0/24 - link_local subnet for containers in pasta mode) -hostssl all all {{ hostvars['fw']['network4']['infra']['server'] }}/32 scram-sha-256 -hostssl all all {{ hostvars['fw']['network6']['infra']['server'] }}/128 scram-sha-256 -hostssl all all {{ hostvars['fw']['network4']['subnet']['lla'] }} scram-sha-256 -hostssl all all {{ hostvars['fw']['network6']['subnet']['lla'] }} scram-sha-256 +hostssl all all {{ hostvars['fw']['network4']['infra']['server'] }}/32 scram-sha-256 +hostssl all all {{ hostvars['fw']['network6']['infra']['server'] }}/128 scram-sha-256 +hostssl all all {{ hostvars['fw']['network4']['subnet']['lla'] }} scram-sha-256 +hostssl all all {{ hostvars['fw']['network6']['subnet']['lla'] }} scram-sha-256 # auth VM -hostssl all all {{ hostvars['fw']['network4']['auth']['server'] }}/32 scram-sha-256 -hostssl all all {{ hostvars['fw']['network6']['auth']['server'] }}/128 scram-sha-256 +hostssl all all {{ hostvars['fw']['network4']['auth']['server'] }}/32 scram-sha-256 +hostssl all all {{ hostvars['fw']['network6']['auth']['server'] }}/128 scram-sha-256 -# app VM (Applications, 192.168.10.13) -hostssl all all {{ hostvars['fw']['network4']['app']['server'] }}/32 scram-sha-256 -hostssl all all {{ hostvars['fw']['network6']['app']['server'] }}/128 scram-sha-256 +# app VM (Applications, 192.168.10.13) +hostssl all all {{ hostvars['fw']['network4']['app']['server'] }}/32 scram-sha-256 +hostssl all all {{ hostvars['fw']['network6']['app']['server'] }}/128 scram-sha-256 diff --git a/config/services/containers/infra/postgresql/config/postgresql.conf.j2 b/config/services/containers/infra/postgresql/config/postgresql.conf.j2 index 19d3aa6..1911f45 100644 --- a/config/services/containers/infra/postgresql/config/postgresql.conf.j2 +++ b/config/services/containers/infra/postgresql/config/postgresql.conf.j2 @@ -3,11 +3,14 @@ #------------------------------------------------------------------------------ # Add settings for extensions here -# Listen_address + +# Listen address listen_addresses = '*' + # Max connections max_connections = 250 -# listen_port + +# Listen port port = {{ services['postgresql']['ports']['tcp'] }} # SSL @@ -18,7 +21,7 @@ ssl_key_file = '/etc/ssl/postgresql/postgresql.key' ssl_ciphers = 'HIGH:!aNULL:!MD5' ssl_prefer_server_ciphers = on -# log +# Log log_destination = 'stderr' log_checkpoints = on log_temp_files = 0 @@ -27,7 +30,7 @@ log_min_duration_statement = 500 # IO track_io_timing = on -## immich_config +# immich_config shared_preload_libraries = 'vchord.so' search_path = '"$user", public' max_wal_size = 5GB @@ -38,4 +41,4 @@ autovacuum_vacuum_scale_factor = 0.1 autovacuum_analyze_scale_factor = 0.05 autovacuum_vacuum_cost_limit = 1000 effective_io_concurrency = 200 -random_page_cost = 1.2 +random_page_cost = 1.2 \ No newline at end of file diff --git a/config/services/containers/infra/postgresql/postgresql.container.j2 b/config/services/containers/infra/postgresql/postgresql.container.j2 index 76ad779..0b42396 100644 --- a/config/services/containers/infra/postgresql/postgresql.container.j2 +++ b/config/services/containers/infra/postgresql/postgresql.container.j2 @@ -19,7 +19,7 @@ Volume=%h/containers/postgresql/data:/var/lib/postgresql:rw Volume=%h/containers/postgresql/config:/config:ro Volume=%h/containers/postgresql/ssl:/etc/ssl/postgresql:ro Volume=%h/containers/postgresql/init:/docker-entrypoint-initdb.d/:ro -Volume=%h/containers/postgresql/backups:/backups:rw +Volume=%h/data/containers/postgresql/backups:/backups:rw Environment="TZ={{ timezone }}" # This option is only for init process, after init custom config file `pg_hba.conf` will control this option. diff --git a/config/services/containers/infra/postgresql/services/postgresql-data-backup@.service b/config/services/containers/infra/postgresql/services/postgresql-data-backup@.service index e40c69b..2a7f5a2 100644 --- a/config/services/containers/infra/postgresql/services/postgresql-data-backup@.service +++ b/config/services/containers/infra/postgresql/services/postgresql-data-backup@.service @@ -13,7 +13,13 @@ StandardError=journal ExecStartPre=/usr/bin/podman exec postgresql sh -c "mkdir -p /backups/%i && chown postgres:root /backups/%i && chmod 770 /backups/%i" # Run the script +## DB Connection ExecStart=/usr/bin/podman exec -u postgres postgresql sh -c 'printf "\\connect %i_db\n" > /backups/%i/pg_%i_$(date "+%%Y-%%m-%%d").sql' +## Avoiding FK/trigger checks +ExecStart=/usr/bin/podman exec -u postgres postgresql sh -c 'printf "SET session_replication_role = replica;\n" >> /backups/%i/pg_%i_$(date "+%%Y-%%m-%%d").sql' +## Data ExecStart=/usr/bin/podman exec -u postgres postgresql sh -c 'pg_dump -U postgres -d %i_db --data-only >> /backups/%i/pg_%i_$(date "+%%Y-%%m-%%d").sql' +## reset session replication role +ExecStart=/usr/bin/podman exec -u postgres postgresql sh -c 'printf "SET session_replication_role = origin;\n" >> /backups/%i/pg_%i_$(date "+%%Y-%%m-%%d").sql' ExecStart=/usr/bin/podman exec -u postgres postgresql sh -c "find /backups/%i -maxdepth 1 -type f -mtime +7 -delete" ExecStart=/usr/bin/podman exec postgresql sh -c "chown -R postgres:root /backups/%i && chmod 660 /backups/%i/*" diff --git a/config/services/systemd/app/btrfs/btrfs-scrub.service.j2 b/config/services/systemd/app/btrfs/btrfs-scrub.service.j2 deleted file mode 100644 index 12aacf3..0000000 --- a/config/services/systemd/app/btrfs/btrfs-scrub.service.j2 +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=BTRFS auto scrub -ConditionPathIsMountPoint={{ storage['btrfs']['mount_point'] }} -RequiresMountsFor={{ storage['btrfs']['mount_point'] }} - -[Service] -Type=oneshot -ExecStart=/usr/bin/btrfs scrub start -Bd {{ storage['btrfs']['mount_point'] }} -Nice=19 -IOSchedulingClass=idle diff --git a/config/services/systemd/common/alloy/config.alloy.j2 b/config/services/systemd/common/alloy/config.alloy.j2 index 24b87a1..3f7b220 100644 --- a/config/services/systemd/common/alloy/config.alloy.j2 +++ b/config/services/systemd/common/alloy/config.alloy.j2 @@ -207,7 +207,7 @@ loki.process "journal_parser" { // Regex to extract the log level from the content. expression = "(?i)(?:level[\"\\s:=]+|\\[|\\s|^)(?Pinfo|warn|warning|error|debug|fatal|critical|trace)(?:[\"\\]\\s]|$)" } - + stage.labels { values = { "level" = "content_level", @@ -237,7 +237,7 @@ loki.process "journal_parser" { loki.process "suricata_json" { forward_to = [loki.write.loki.receiver] stage.json { - expressions = { + expressions = { event_type = "event_type", src_ip = "src_ip", severity = "alert.severity", @@ -253,7 +253,7 @@ loki.process "suricata_json" { loki.process "caddy_json" { forward_to = [loki.write.loki.receiver] stage.json { - expressions = { + expressions = { status = "status", method = "method", remote_ip = "remote_ip", diff --git a/config/services/systemd/common/crowdsec/acquis.d/suricata.yaml b/config/services/systemd/common/crowdsec/acquis.d/suricata.yaml index d743905..d1c25ba 100644 --- a/config/services/systemd/common/crowdsec/acquis.d/suricata.yaml +++ b/config/services/systemd/common/crowdsec/acquis.d/suricata.yaml @@ -2,4 +2,4 @@ filenames: - /var/log/suricata/eve.json labels: - type: suricata + type: suricata-evelogs diff --git a/config/services/systemd/common/crowdsec/etc/config.yaml.j2 b/config/services/systemd/common/crowdsec/etc/config.yaml.j2 index 0563a4b..86d0da2 100644 --- a/config/services/systemd/common/crowdsec/etc/config.yaml.j2 +++ b/config/services/systemd/common/crowdsec/etc/config.yaml.j2 @@ -27,7 +27,7 @@ db_config: type: sqlite db_path: /var/lib/crowdsec/data/crowdsec.db #max_open_conns: 100 - #user: + #user: #password: #db_name: #host: diff --git a/config/services/systemd/common/kopia/kopia-backup.service.j2 b/config/services/systemd/common/kopia/kopia-backup.service.j2 index fb82872..96bce14 100644 --- a/config/services/systemd/common/kopia/kopia-backup.service.j2 +++ b/config/services/systemd/common/kopia/kopia-backup.service.j2 @@ -20,11 +20,8 @@ ProtectSystem=strict ProtectHome=tmpfs InaccessiblePaths=/boot /root -{% if node['name'] == 'infra' %} -BindReadOnlyPaths={{ node['home_path'] }}/containers/postgresql/backups -{% elif node['name'] == 'app' %} BindReadOnlyPaths={{ node['home_path'] }}/data -{% endif %} + # In root namescope, %u always bring 0 BindPaths=/etc/kopia BindPaths=/etc/secrets/{{ kopia_uid }} @@ -36,14 +33,8 @@ ExecStartPre=/usr/bin/kopia repository connect server \ --override-username={{ node['name'] }} \ --override-hostname={{ node['name'] }}.{{ domain['internal'] }} -{% if node['name'] == 'infra' %} -ExecStart=/usr/bin/kopia snapshot create \ - {{ node['home_path'] }}/containers/postgresql/backups -{% elif node['name'] == 'app' %} ExecStart=/usr/bin/kopia snapshot create \ {{ node['home_path'] }}/data -{% endif %} - [Install] WantedBy=multi-user.target diff --git a/config/services/systemd/common/kopia/kopia.env.j2 b/config/services/systemd/common/kopia/kopia.env.j2 index ebee7f7..70477b1 100644 --- a/config/services/systemd/common/kopia/kopia.env.j2 +++ b/config/services/systemd/common/kopia/kopia.env.j2 @@ -1,4 +1,4 @@ -KOPIA_PASSWORD={{ hostvars['console']['kopia']['user'][node['name']] }} +KOPIA_PASSWORD={{ hostvars['console']['kopia']['users'][node['name']] }} KOPIA_CONFIG_PATH=/etc/kopia/repository.config KOPIA_CACHE_DIRECTORY=/var/cache/kopia KOPIA_LOG_DIR=/var/cache/kopia/logs diff --git a/config/services/systemd/console/code-server/.vscode/settings.json b/config/services/systemd/console/code-server/.vscode/settings.json new file mode 100644 index 0000000..ffcb138 --- /dev/null +++ b/config/services/systemd/console/code-server/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "python.useEnvironmentsExtension": false, + "ansible.lightspeed.enabled": false, + "ansible.lightspeed.suggestions.enabled": false, + "ansible.validation.enabled": true, + "ansible.validation.lint.enabled": true, + "files.associations": { + "**/ansible/**/*.yaml": "ansible", + "*.container*": "ini", + "*.service*": "ini", + "*.timer*": "ini", + "*containerfile*": "dockerfile", + "*.link*": "ini", + "*.netdev*": "ini", + "*.network*": "ini", + "*.xml*": "xml", + "named.conf*": "cpp", + "*.conf*": "ini", + "*.toml": "ini", + "*.json.*": "json", + "*.alloy.*": "cpp" + }, + "ansible.python.interpreterPath": "/bin/python3", + "yaml.customTags": [ + "!vault scalar" + ], + "workbench.hover.delay": 9999999, +} diff --git a/config/services/systemd/console/code-server/code-server.service b/config/services/systemd/console/code-server/code-server.service new file mode 100644 index 0000000..e70d89e --- /dev/null +++ b/config/services/systemd/console/code-server/code-server.service @@ -0,0 +1,14 @@ +[Unit] +Description=code-server user service + +[Service] +Type=simple +ExecStart=/usr/bin/code-server \ + --auth none \ + --bind-addr 127.0.0.1:2080 \ + %h/workspace +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/config/services/systemd/console/wsl-alive/wsl-alive.service b/config/services/systemd/console/wsl-alive/wsl-alive.service new file mode 100644 index 0000000..779e3d3 --- /dev/null +++ b/config/services/systemd/console/wsl-alive/wsl-alive.service @@ -0,0 +1,10 @@ +[Unit] +Description=Keep Distro Alive + +[Service] +ExecStart=/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -WindowStyle Hidden -NonInteractive -NoProfile -Command "while ($true) { Start-Sleep -Seconds 3600 }" +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/config/services/systemd/fw/ddns/ddns.sh b/config/services/systemd/fw/ddns/ddns.sh index 4d1bcb0..f564544 100644 --- a/config/services/systemd/fw/ddns/ddns.sh +++ b/config/services/systemd/fw/ddns/ddns.sh @@ -1,7 +1,7 @@ #!/bin/bash ## Change Log format as logfmt (refactoring) -# ddns.sh -d domain [-t ] [-p] [-r] [-c] +# ddns.sh -d domain [-t ] [-p] [-r] # Default Information DOMAIN="" @@ -17,7 +17,7 @@ CURRENT_IP="" # usage() function usage() { - echo "Usage: $0 -d \"domain\" [-t \"ttl\"] [-p] [-r] [-c]" + echo "Usage: $0 -d \"domain\" [-t \"ttl\"] [-p] [-r]" echo "-d : Specify the domain to update" echo "-t : Specify the TTL(Time to live)" echo "-p: Specify the cloudflare proxy to use" @@ -109,7 +109,7 @@ if [[ -z "$CURRENT_IP" || "$CURRENT_IP" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.| CURRENT_IP=$(curl -sf "https://api.ipify.org") fi if [ "$CURRENT_IP" == "" ]; then - log "Error" "Can't get an IP" + log "error" "Can't get an IP" exit 1 fi @@ -218,8 +218,8 @@ delete_dns_record() # Get DNS A, and CNAME record A_DNS_RECORD=$(get_dns_record "A" "$DOMAIN") -S_DNS_RECORD=$(get_dns_record "cname" "*.$DOMAIN") -W_DNS_RECORD=$(get_dns_record "cname" "www.$DOMAIN") +S_DNS_RECORD=$(get_dns_record "CNAME" "*.$DOMAIN") +W_DNS_RECORD=$(get_dns_record "CNAME" "www.$DOMAIN") # Delete DNS record with Delete flag if [ "$DELETE_FLAG" == "true" ]; then @@ -232,13 +232,13 @@ if [ "$DELETE_FLAG" == "true" ]; then fi if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then S_DNS_ID="$(echo $S_DNS_RECORD | jq -r '.result[0].id')" - delete_dns_record "cname" "$S_DNS_ID" + delete_dns_record "CNAME" "$S_DNS_ID" log "info" "sub DNS record is deleted" FLAG="true" fi if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then W_DNS_ID="$(echo $W_DNS_RECORD | jq -r '.result[0].id')" - delete_dns_record "cname" "$W_DNS_ID" + delete_dns_record "CNAME" "$W_DNS_ID" log "info" "www DNS record is deleted" FLAG="true" fi @@ -272,14 +272,14 @@ if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # sub DNS S_DNS_TTL="$(echo $S_DNS_RECORD | jq -r '.result[0].ttl')" S_DNS_PROXIED="$(echo $S_DNS_RECORD | jq -r '.result[0].proxied')" if [ "$S_DNS_CONTENT" != "$DOMAIN" -o "$S_DNS_TTL" != "$C_TTL" -o "$S_DNS_PROXIED" != "$PROXIED" ]; then - update_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" "$S_DNS_ID" - log "info" "Sub DNS record is successfully changed Domain: $S_DNS_CONTENT to *.$DOMAIN cname: $DOMAIN TTL: $S_DNS_TTL to $C_TTL proxied: $S_DNS_PROXIED to $PROXIED" + update_dns_record "CNAME" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" "$S_DNS_ID" + log "info" "Sub DNS record is successfully changed Domain: $S_DNS_CONTENT to *.$DOMAIN CNAME: $DOMAIN TTL: $S_DNS_TTL to $C_TTL proxied: $S_DNS_PROXIED to $PROXIED" else - log "info" "Sub DNS record is not changed Domain: *.$DOMAIN cname: $DOMAIN TTL: $C_TTL proxied: $PROXIED" + log "info" "Sub DNS record is not changed Domain: *.$DOMAIN CNAME: $DOMAIN TTL: $C_TTL proxied: $PROXIED" fi else # sub DNS record does not exist - create_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" - log "info" "Sub DNS record is successfully created Domain: *.$DOMAIN cname: $DOMAIN TTL: $C_TTL proxied: $PROXIED" + create_dns_record "CNAME" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" + log "info" "Sub DNS record is successfully created Domain: *.$DOMAIN CNAME: $DOMAIN TTL: $C_TTL proxied: $PROXIED" fi if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # www DNS record exist @@ -288,12 +288,12 @@ if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # www DNS W_DNS_TTL="$(echo $W_DNS_RECORD | jq -r '.result[0].ttl')" W_DNS_PROXIED="$(echo $W_DNS_RECORD | jq -r '.result[0].proxied')" if [ "$W_DNS_CONTENT" != "$DOMAIN" -o "$W_DNS_TTL" != "$C_TTL" -o "$W_DNS_PROXIED" != "$PROXIED" ]; then - update_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" "$W_DNS_ID" - log "info" "www DNS record is successfully changed Domain: $W_DNS_CONTENT to www.$DOMAIN cname: $DOMAIN TTL: $W_DNS_TTL to $C_TTL proxied: $W_DNS_PROXIED to $PROXIED" + update_dns_record "CNAME" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" "$W_DNS_ID" + log "info" "www DNS record is successfully changed Domain: $W_DNS_CONTENT to www.$DOMAIN CNAME: $DOMAIN TTL: $W_DNS_TTL to $C_TTL proxied: $W_DNS_PROXIED to $PROXIED" else - log "info" "www DNS record is not changed Domain: www.$DOMAIN cname: $DOMAIN TTL: $C_TTL proxied: $PROXIED" + log "info" "www DNS record is not changed Domain: www.$DOMAIN CNAME: $DOMAIN TTL: $C_TTL proxied: $PROXIED" fi else # www DNS record does not exist - create_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" - log "info" "www DNS record is successfully created Domain: www.$DOMAIN cname: $DOMAIN TTL: $C_TTL proxied: $PROXIED" + create_dns_record "CNAME" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" + log "info" "www DNS record is successfully created Domain: www.$DOMAIN CNAME: $DOMAIN TTL: $C_TTL proxied: $PROXIED" fi diff --git a/config/services/systemd/vmm/btrfs/btrfs-scrub.service.j2 b/config/services/systemd/vmm/btrfs/btrfs-scrub.service.j2 new file mode 100644 index 0000000..9ee6e12 --- /dev/null +++ b/config/services/systemd/vmm/btrfs/btrfs-scrub.service.j2 @@ -0,0 +1,10 @@ +[Unit] +Description=BTRFS auto scrub +ConditionPathIsMountPoint={{ hardware['btrfs']['mount_point'] }} +RequiresMountsFor={{ hardware['btrfs']['mount_point'] }} + +[Service] +Type=oneshot +ExecStart=/usr/bin/btrfs scrub start -Bd {{ hardware['btrfs']['mount_point'] }} +Nice=19 +IOSchedulingClass=idle diff --git a/config/services/systemd/app/btrfs/btrfs-scrub.timer.j2 b/config/services/systemd/vmm/btrfs/btrfs-scrub.timer.j2 similarity index 75% rename from config/services/systemd/app/btrfs/btrfs-scrub.timer.j2 rename to config/services/systemd/vmm/btrfs/btrfs-scrub.timer.j2 index 9bd2559..5b9216b 100644 --- a/config/services/systemd/app/btrfs/btrfs-scrub.timer.j2 +++ b/config/services/systemd/vmm/btrfs/btrfs-scrub.timer.j2 @@ -1,5 +1,5 @@ [Unit] -Description=Monthly BTRFS auto scrub +Description=Monthly BTRFS auto scrub [Timer] OnCalendar=*-*-01 04:00:00 diff --git a/config/services/systemd/vmm/libvirt/seeds/user-data.j2 b/config/services/systemd/vmm/libvirt/seeds/user-data.j2 index 9860ac3..33f1d73 100644 --- a/config/services/systemd/vmm/libvirt/seeds/user-data.j2 +++ b/config/services/systemd/vmm/libvirt/seeds/user-data.j2 @@ -11,7 +11,7 @@ users: - name: {{ target_vm }} uid: {{ hostvars[target_vm]['node']['uid'] }} gecos: {{ target_vm }} - primary_group: svadmins + primary_group: {{ hostvars[target_vm]['node']['group'] }} groups: sudo lock_passwd: false passwd: {{ hostvars['console']['sudo']['hash'][target_vm] }} diff --git a/config/services/systemd/vmm/libvirt/xml/storages/data-pool.xml b/config/services/systemd/vmm/libvirt/xml/storages/data-pool.xml new file mode 100644 index 0000000..8bfd284 --- /dev/null +++ b/config/services/systemd/vmm/libvirt/xml/storages/data-pool.xml @@ -0,0 +1,8 @@ + + + + data-pool + + /var/lib/libvirt/data + + diff --git a/config/services/systemd/vmm/libvirt/xml/storages/images-pool.xml b/config/services/systemd/vmm/libvirt/xml/storages/images-pool.xml index 4bd5f25..1d6b857 100644 --- a/config/services/systemd/vmm/libvirt/xml/storages/images-pool.xml +++ b/config/services/systemd/vmm/libvirt/xml/storages/images-pool.xml @@ -1,8 +1,8 @@ - + images-pool /var/lib/libvirt/images - + diff --git a/config/services/systemd/vmm/libvirt/xml/storages/seeds-pool.xml b/config/services/systemd/vmm/libvirt/xml/storages/seeds-pool.xml index 9511058..9ce8ea1 100644 --- a/config/services/systemd/vmm/libvirt/xml/storages/seeds-pool.xml +++ b/config/services/systemd/vmm/libvirt/xml/storages/seeds-pool.xml @@ -4,5 +4,5 @@ seeds-pool /var/lib/libvirt/seeds - + diff --git a/config/services/systemd/vmm/libvirt/xml/vms/vms.xml.j2 b/config/services/systemd/vmm/libvirt/xml/vms/vms.xml.j2 index 6fff61e..fef60f1 100644 --- a/config/services/systemd/vmm/libvirt/xml/vms/vms.xml.j2 +++ b/config/services/systemd/vmm/libvirt/xml/vms/vms.xml.j2 @@ -4,6 +4,7 @@ {{ hostvars[target_vm]['vm']['cpu'] }} {{ hostvars[target_vm]['vm']['shares'] }} + {{ hostvars[target_vm]['vm']['cpu_pin'] | indent(8) }} hvm @@ -12,7 +13,7 @@ /usr/share/OVMF/OVMF_CODE_4M.ms.fd - /var/lib/libvirt/qemu/nvram/{{ hostvars[target_vm]['vm']['name'] }}_VARS.fd + {{ node['libvirt_path'] }}/qemu/nvram/{{ hostvars[target_vm]['vm']['name'] }}_VARS.fd @@ -26,12 +27,12 @@ - + - + @@ -61,7 +62,7 @@
{% if target_vm == 'app' %} -{% for device in hostvars[target_vm]['vm']['pass_through'].values() %} +{% set device=hostvars['vmm']['hardware']['igpu'] %} @@ -69,8 +70,12 @@
-{% endfor %} -{% endif %} + + + + + +{% endif %} diff --git a/data/create_all_structure.sh b/data/create_all_structure.sh deleted file mode 100755 index aa8a6d8..0000000 --- a/data/create_all_structure.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -FILE_PATH="$HOME/workspace/homelab/docs/archives/textfiles/$(date "+%Y-%m-%d")" - -mkdir -p $FILE_PATH - -echo "--- Ansible contents ---" > "$FILE_PATH/ansible.txt" -find ~/workspace/homelab/ansible -type f \ - -print0 | \ - sort -z | \ - xargs -0 awk 'FNR==1{print "\n\n---------------------\nFILE PATH: " FILENAME "\n---------------------\n"}1' \ - >> "$FILE_PATH/ansible.txt" -echo "---------------------" | sort >> "$FILE_PATH/ansible.txt" - -echo "--- Data contents ---" > "$FILE_PATH/data.txt" -find ~/workspace/homelab/data -type f \ - ! -path "*volumes*" \ - ! -name "*.deb" \ - ! -name "*.gz" \ - ! -name "*.qcow2" \ - ! -name "*.iso" \ - ! -name "*.gpg" \ - ! -name "*.sql" \ - -print0 | \ - sort -z | \ - xargs -0 awk 'FNR==1{print "\n\n---------------------\nFILE PATH: " FILENAME "\n---------------------\n"}1' \ - >> "$FILE_PATH/data.txt" -echo "---------------------" | sort >> "$FILE_PATH/data.txt" - -echo "--- Config contents ---" > "$FILE_PATH/config.txt" -find ~/workspace/homelab/config -type f \ - ! -path "*/grafana/etc/dashboards*" \ - ! -name "*.sql" \ - -print0 | \ - sort -z | \ - xargs -0 awk 'FNR==1{print "\n\n---------------------\nFILE PATH: " FILENAME "\n---------------------\n"}1' \ - >> "$FILE_PATH/config.txt" -echo "---------------------" | sort >> "$FILE_PATH/config.txt" - - -echo "--- Docs contents ---" > "$FILE_PATH/docs.txt" -find ~/workspace/homelab/docs -type f \ - ! -path "*archives/*" \ - ! -name "*.deb" \ - ! -name "*.gz" \ - ! -name "*.qcow2" \ - ! -name "*.iso" \ - ! -name "*.gpg" \ - ! -name "*.sql" \ - -print0 | \ - sort -z | \ - xargs -0 awk 'FNR==1{print "\n\n---------------------\nFILE PATH: " FILENAME "\n---------------------\n"}1' \ - >> "$FILE_PATH/docs.txt" -echo "---------------------" | sort >> "$FILE_PATH/docs.txt" - -cp ~/workspace/homelab/README.md $FILE_PATH/README.md \ No newline at end of file diff --git a/data/ilnmors_root_ca.crt b/data/ilnmors_root_ca.crt deleted file mode 100644 index 39fe146..0000000 --- a/data/ilnmors_root_ca.crt +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIBijCCATCgAwIBAgIRAKPu8PZdfsvjvryosyWb01owCgYIKoZIzj0EAwIwIzEh -MB8GA1UEAxMYaWxubW9ycy5pbnRlcm5hbCBSb290IENBMB4XDTI1MTIxOTA0MDM0 -N1oXDTM1MTIxNzA0MDM0N1owIzEhMB8GA1UEAxMYaWxubW9ycy5pbnRlcm5hbCBS -b290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKDZLQ+47J72K9281V6qG -I8kBScUpJOppbbIaCwi94dp6Tqbe9PfT4eChOSt2lkmb7bG0PmgfYOXv/FUvgFGP -DKNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0O -BBYEFCZMiE3SKrzUqlHM6BYDJYkYyelRMAoGCCqGSM49BAMCA0gAMEUCIC3+tXyt -6uz75leUXhQsa0gQ/QfPd/dtjRQvuRPURAZ1AiEAjTFJe7cID6rSByF3e0rhgyeL -d6BE/tcQ7ymDBWUTGn0= ------END CERTIFICATE----- diff --git a/docs/notes/.gitkeep b/data/images/seeds/.gitkeep similarity index 100% rename from docs/notes/.gitkeep rename to data/images/seeds/.gitkeep diff --git a/docs/services/console/git.md b/data/kopia/.gitkeep similarity index 100% rename from docs/services/console/git.md rename to data/kopia/.gitkeep diff --git a/data/vmm_init/.gitkeep b/data/vmm_init/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/vmm_init/grub.d/iommu.cfg b/data/vmm_init/grub.d/iommu.cfg deleted file mode 100644 index 6eda4e4..0000000 --- a/data/vmm_init/grub.d/iommu.cfg +++ /dev/null @@ -1 +0,0 @@ -GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt" diff --git a/data/vmm_init/modprobe.d/vfio.conf b/data/vmm_init/modprobe.d/vfio.conf deleted file mode 100644 index d03d405..0000000 --- a/data/vmm_init/modprobe.d/vfio.conf +++ /dev/null @@ -1,3 +0,0 @@ -options vfio-pci ids=8086:46d4,1b21:1064 -softdep i915 pre: vfio-pci -softdep ahci pre: vfio-pci \ No newline at end of file diff --git a/data/vmm_init/network/00-vmm-eth0.link b/data/vmm_init/network/00-vmm-eth0.link deleted file mode 100644 index 1119415..0000000 --- a/data/vmm_init/network/00-vmm-eth0.link +++ /dev/null @@ -1,5 +0,0 @@ -[Match] -MACAddress=c8:ff:bf:05:aa:b0 - -[Link] -Name=eth0 diff --git a/data/vmm_init/network/01-vmm-eth1.link b/data/vmm_init/network/01-vmm-eth1.link deleted file mode 100644 index 5f72662..0000000 --- a/data/vmm_init/network/01-vmm-eth1.link +++ /dev/null @@ -1,5 +0,0 @@ -[Match] -MACAddress=c8:ff:bf:05:aa:b1 - -[Link] -Name=eth1 diff --git a/data/vmm_init/network/10-vmm-br0.netdev b/data/vmm_init/network/10-vmm-br0.netdev deleted file mode 100644 index 6ec2b6d..0000000 --- a/data/vmm_init/network/10-vmm-br0.netdev +++ /dev/null @@ -1,3 +0,0 @@ -[NetDev] -Name=br0 -Kind=bridge diff --git a/data/vmm_init/network/11-vmm-br1.netdev b/data/vmm_init/network/11-vmm-br1.netdev deleted file mode 100644 index 3f00292..0000000 --- a/data/vmm_init/network/11-vmm-br1.netdev +++ /dev/null @@ -1,7 +0,0 @@ -[NetDev] -Name=br1 -Kind=bridge - -[Bridge] -VLANFiltering=true -DefaultPVID=1 diff --git a/data/vmm_init/network/12-vmm-vlan1.netdev b/data/vmm_init/network/12-vmm-vlan1.netdev deleted file mode 100644 index bbe3596..0000000 --- a/data/vmm_init/network/12-vmm-vlan1.netdev +++ /dev/null @@ -1,6 +0,0 @@ -[NetDev] -Name=vlan1 -Kind=vlan - -[VLAN] -Id=1 diff --git a/data/vmm_init/network/13-vmm-vlan10.netdev b/data/vmm_init/network/13-vmm-vlan10.netdev deleted file mode 100644 index 70257b8..0000000 --- a/data/vmm_init/network/13-vmm-vlan10.netdev +++ /dev/null @@ -1,6 +0,0 @@ -[NetDev] -Name=vlan10 -Kind=vlan - -[VLAN] -Id=10 diff --git a/data/vmm_init/network/14-vmm-vlan20.netdev b/data/vmm_init/network/14-vmm-vlan20.netdev deleted file mode 100644 index 01c2af6..0000000 --- a/data/vmm_init/network/14-vmm-vlan20.netdev +++ /dev/null @@ -1,6 +0,0 @@ -[NetDev] -Name=vlan20 -Kind=vlan - -[VLAN] -Id=20 diff --git a/data/vmm_init/network/20-vmm-eth0.network b/data/vmm_init/network/20-vmm-eth0.network deleted file mode 100644 index f26bfe6..0000000 --- a/data/vmm_init/network/20-vmm-eth0.network +++ /dev/null @@ -1,6 +0,0 @@ -[Match] -Name=eth0 - -[Network] -Bridge=br0 -LinkLocalAddressing=false diff --git a/data/vmm_init/network/21-vmm-eth1.network b/data/vmm_init/network/21-vmm-eth1.network deleted file mode 100644 index 19339dd..0000000 --- a/data/vmm_init/network/21-vmm-eth1.network +++ /dev/null @@ -1,15 +0,0 @@ -[Match] -Name=eth1 - -[Network] -Bridge=br1 -LinkLocalAddressing=false - -[BridgeVLAN] -VLAN=1 -PVID=true -EgressUntagged=true - -[BridgeVLAN] -VLAN=10 -VLAN=20 diff --git a/data/vmm_init/network/22-vmm-br0.network b/data/vmm_init/network/22-vmm-br0.network deleted file mode 100644 index 1eae45a..0000000 --- a/data/vmm_init/network/22-vmm-br0.network +++ /dev/null @@ -1,5 +0,0 @@ -[Match] -Name=br0 - -[Network] -LinkLocalAddressing=false diff --git a/data/vmm_init/network/23-vmm-br1.network b/data/vmm_init/network/23-vmm-br1.network deleted file mode 100644 index ac0e65a..0000000 --- a/data/vmm_init/network/23-vmm-br1.network +++ /dev/null @@ -1,17 +0,0 @@ -[Match] -Name=br1 - -[Network] -VLAN=vlan1 -VLAN=vlan10 -VLAN=vlan20 -LinkLocalAddressing=false - -[BridgeVLAN] -VLAN=1 -PVID=yes -EgressUntagged=true - -[BridgeVLAN] -VLAN=10 -VLAN=20 diff --git a/data/vmm_init/network/24-vmm-vlan1.network b/data/vmm_init/network/24-vmm-vlan1.network deleted file mode 100644 index 3ebb2cd..0000000 --- a/data/vmm_init/network/24-vmm-vlan1.network +++ /dev/null @@ -1,28 +0,0 @@ -[Match] -Name=vlan1 - -[Network] -# IPv4 -Address=192.168.1.10/24 -# IPv6 -Address=fd00:1::10/64 - -[RoutingPolicyRule] -From=192.168.1.10/32 -Table=1 -Priority=100 - -[Route] -Destination=192.168.1.0/24 -Scope=link -Table=1 - -[RoutingPolicyRule] -From=fd00:1::10/128 -Table=61 -Priority=100 - -[Route] -Destination=fd00:1::/64 -Scope=link -Table=61 diff --git a/data/vmm_init/network/25-vmm-vlan10.network b/data/vmm_init/network/25-vmm-vlan10.network deleted file mode 100644 index 62cc6e8..0000000 --- a/data/vmm_init/network/25-vmm-vlan10.network +++ /dev/null @@ -1,32 +0,0 @@ -[Match] -Name=vlan10 -[Network] -RequiredForOnline=false -# IPv4 -Address=192.168.10.10/24 -Gateway=192.168.10.1 -DNS=192.168.10.2 -# IPv6 -Address=fd00:10::10/64 -Gateway=fd00:10::1 -DNS=fd00:10::2 - -[RoutingPolicyRule] -From=192.168.10.10/32 -Table=2 -Priority=100 - -[Route] -Destination=0.0.0.0/0 -Gateway=192.168.10.1 -Table=2 - -[RoutingPolicyRule] -From=fd00:10::10/128 -Table=62 -Priority=100 - -[Route] -Destination=::/0 -Gateway=fd00:10::1 -Table=62 diff --git a/data/vmm_init/nftables.conf b/data/vmm_init/nftables.conf deleted file mode 100644 index 34d2c86..0000000 --- a/data/vmm_init/nftables.conf +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/sbin/nft -f - -flush ruleset - -define HOSTS4_CONSOLE = { 192.168.1.20, 192.168.99.20 } -define HOSTS6_CONSOLE = { fd00:1::20, fd00:99::20 } -define PORTS_SSH = 22 - -table inet filter { - chain input { - type filter hook input priority 0; policy drop; - ct state invalid drop comment "deny invalid connection" - ct state established, related accept comment "allow all connection already existing" - iifname "lo" accept comment "allow local connection" - meta l4proto { icmp, icmpv6 } accept comment "allow icmp connection: > VMM" - ip saddr $HOSTS4_CONSOLE tcp dport $PORTS_SSH accept comment "allow ipv4 ssh connection: CONSOLE > VMM" - ip6 saddr $HOSTS6_CONSOLE tcp dport $PORTS_SSH accept comment "allow ipv6 ssh connection: CONSOLE > VMM" - } - chain forward { - type filter hook forward priority 0; policy drop; - } - chain output { - type filter hook output priority 0; policy accept; - } -} diff --git a/data/vmm_init/ssh/local_ssh_ca.pub b/data/vmm_init/ssh/local_ssh_ca.pub deleted file mode 100644 index 8d2544b..0000000 --- a/data/vmm_init/ssh/local_ssh_ca.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJtBbAyORSd3qece5jHnEFrJPR7QxIzeIUsTEYoBLMKd LOCAL_SSH_CA diff --git a/data/vmm_init/ssh/sshd_config.d/prohibit_root.conf b/data/vmm_init/ssh/sshd_config.d/prohibit_root.conf deleted file mode 100644 index 7ff52c7..0000000 --- a/data/vmm_init/ssh/sshd_config.d/prohibit_root.conf +++ /dev/null @@ -1 +0,0 @@ -PermitRootLogin no diff --git a/data/vmm_init/ssh/sshd_config.d/ssh_ca.conf b/data/vmm_init/ssh/sshd_config.d/ssh_ca.conf deleted file mode 100644 index 10c8405..0000000 --- a/data/vmm_init/ssh/sshd_config.d/ssh_ca.conf +++ /dev/null @@ -1 +0,0 @@ -TrustedUserCAKeys /etc/ssh/local_ssh_ca.pub diff --git a/data/vmm_init/sysctl.d/bridge.conf b/data/vmm_init/sysctl.d/bridge.conf deleted file mode 100644 index 3c35632..0000000 --- a/data/vmm_init/sysctl.d/bridge.conf +++ /dev/null @@ -1,3 +0,0 @@ -net.bridge.bridge-nf-call-ip6tables = 0 -net.bridge.bridge-nf-call-iptables = 0 -net.bridge.bridge-nf-call-arptables = 0 \ No newline at end of file diff --git a/docs/adr/001-architecture.md b/docs/adr/001-architecture.md deleted file mode 100644 index 87f4456..0000000 --- a/docs/adr/001-architecture.md +++ /dev/null @@ -1,72 +0,0 @@ -# ADR 001 - Architecture - -## Date - -- Feb/23/2026 - - First documentation -- Mar/4/2026 - - Refining sentences - -## Status - -- Accepted - -## Context - -- Maintaining multi nodes requires a huge amount of resources, including hardware, electricity, even administrative efforts -- All units which responsible for a single role should follow the Principle of Least Privilege (PoLP). -- All units should be interchangeable on standard to avoid vendor lock-in. - -## Consideration - -### Hypervisor - -- Proxmox Virutal Environment (PVE) - - Based on Debian. - - PVE uses `qm` command which is not a standard to implement the virtual environment. -- VMware ESXi - - Based on UNIX, deveoped by VMware (Licence is not free) -- Hyper-V - - Based on Microsoft Windows (Licence is not free) -- Debian Stable - - Based on standard linux (conservative) - - Standard virtualization technology 'Libvirt, QEMU, KVM' - -### Container - -- Docker - - Daemon is used to run containers - - Root authority required - - Socket and network problem is complex (Docker bridge) - - docker-compose is an orchestration tool -- Rootless Podman - - Daemonless design - - Root authority not required - - Orchestration is integrated into systemd - - PASTA dumps packet via host-gateway -- K8S, K3S - - HA is based on reprovisioning - - Guarantee availability to create and destroy node dynamically - -### IaC - -- Terraform - - Strength for initiating low-level and dynamic multi node environment -- Ansible - - Declaritive and easy yaml grammar - - SSH is the way to set - -## Decisions - -- Use Libvirt/KVM/QEMU on pure linux (Debian stable). -- Separate all services by VM, and podman rootless containers without K3S. - - Orchestration stack is not needed in single node system - - Services will be defined by Quadelt to integrate into systemd and to manage them declaratively - - IaC will be implemented by Ansible only declaratively -- All VMs and services are isolated logically by VLAN and nftables - -## Consequences - -- All VMs have independent borderline by VLAN and nftables -- All services have independent namespaces by podman subuid without daemon -- Ansible can manage all configurations of services and VMs declaratively diff --git a/docs/adr/002-network.md b/docs/adr/002-network.md deleted file mode 100644 index c432fdf..0000000 --- a/docs/adr/002-network.md +++ /dev/null @@ -1,63 +0,0 @@ -# ADR 002 - Network - -## Date - -- Feb/23/2026 - - First documentation - - -## Status - -- Accepted - -## Context - -- All L3 communications should be contolled by central firewall node. -- Every firewall rule should be managed by code, not clicks. -- Every edge node takes charge of L2 communication rules. -- IPv4 and IPv6 dual stack should be supported for future network environment. - -## Consideration - -### Firewall - -- OPNSense/pfSense - - vendor lock-in - - GUI environment (WebGUI) can contain vulnerability - - It is hard to manage configurations by IaC -- iptables - - Previous standard of Linux - - IPv4 and IPv6 configuration is separated (no inet) -- nftables - - New standard of Linux - - English grammar friendly - - IPv4 and IPv6 configuration can be set on the same table (inet) - -### Flat network structure -- LAN only - - L2 communication doesn't need to pass through gateway - - They use MAC address with ARP. Unicast communication is hard to manage. - - It is hard to manage and apply the policy centrally - -## Decisions - -- Categorize all nodes in 4 roles 'client', 'server', 'user', 'wg0; vpn connections' -- Implement role separation with VLAN tagging on L2 switch (systemd-networkd bridge) - - VLAN 1: client (vmm, console, nas) - - VLAN 10: server (vmm, infra, auth, app) - - VLAN 20: user (DHCP allocated devices) - - wg0: VPN connections -- Manage the rules based on roles fundamentally, furthermore manage them based on ip and ports when it is needed -- All L3 communication which needs to pass gateway should be on control of firewall (fw) -- All nodes including firewall uses nftables (modern standard) to manage the packets based on zone concept -- IPv6 has two track strategy - - Client and server, wg nodes has static ULA IP, and use NAT66 for permanency - - User nodes has GUA SLAAC IP from ISP for compatibility - - -## Consequences - -- Firewall takes charge of L3 communications -- Each nodes takes charge of L2 communications and communication from FW -- All nodes can communicate under both IPv4 and IPv6 -- All policies can be managed by Code diff --git a/docs/adr/003-pki.md b/docs/adr/003-pki.md deleted file mode 100644 index 14f3cf9..0000000 --- a/docs/adr/003-pki.md +++ /dev/null @@ -1,57 +0,0 @@ -# ADR 003 - PKI - -## Date - -- Feb/23/2026 - - First documentation -- Mar/06/2026 - - Add expiry date observation way - -## Status - -- Accepted - -## Context - -- All communications except loop-back, should be encrypted -- ssh, and TLS communications needs key and certificates -- Public CA never issues for private domain, '.internal' -- Automate issuing and renewing certificates -- Revocation is not needed in this single and small environment. - -## Consideration - -### Automate protocol - -- JWK/JWT provisioner - - It is hard to manage pre-shared secret values than ACME (Especially nsupdate) -- authorized_keys - - When the nodes are increased, it is hard to manage authorized_key. - - SSH ca.pub allow all the certificates signed by ca key, so it is not needed to manage authroized_keys from each hosts. - -### Revocation - -- CRL/OCSP/OCSP-stappling - - All long-term certificates are managed manually - - All short-term certificates are managed by ACME - - When the certificates are leaked, it is easier to change intermediate CA itself - -## Decisions - -- Operate private CA - - Root CA (Store on coldstorage) - 10 years - - Intermediate CA (Online server as Step-CA) - 5 years - - SSH CA - No period -- Manage certificates with two track - - ACME with nsupdate (using private DNS) for web services via Caddy - 90 days - - Manual issuing and managing leaf certificate for infra services for independency - 2.5 years - - All manual issuing leaf certificate expiry date is observed by x509-exporter on infra vm -- Manage SSH certificates - - *-cert.pub for host (with -h options) - - *-cert.pub for client (without -h options) - -## Consequences - -- Private PKI is operated -- Private SSH CA is operated -- All external/internal communication is encrypted as TLS re-encryption. (E2EE) diff --git a/docs/adr/004-dns.md b/docs/adr/004-dns.md deleted file mode 100644 index d88d5de..0000000 --- a/docs/adr/004-dns.md +++ /dev/null @@ -1,52 +0,0 @@ -# ADR 004 - DNS - -## Date - -- Feb/23/2026 - - First documentation - - -## Status - -- Accepted - -## Context - -- Private authoritative DNS is required to use private reserved root domain (.internal) -- Split horizon DNS needs DNS resolver, because authoritative DNS must not send queries to other DNS. -- Automatical issuing certificates needs private authoritative DNS which supports nsupdate (RFC 2136) - -## Consideration - -### Resolver DNS -- AdGuard Home - - More powerful query routing than blocky - - Web UI dependency - - Extra function which is not useful (DHCP, etc ..) -- Unbound DNS - - Cache and forward zone management is powerful - - more complex than blocky - - cache function is not that needed in this environment - - Internal authoritative DNS only takes charge of internal communication - - All security function is delegated to public DNS like cloudflare (DNSSEC, etc) - -## Decisions - -- Operate BIND9 as authoritative DNS - - BIND9 is developed by ISC as de facto standard of authoritative DNS - - It supports nsupdate perfectly - - Use 2 forward zones - - ilnmors.com for split horizon DNS - - ilnmors.internal for internal DNS - - Uses 4 PTR zones - - Client vlan ipv4, v6 PTR zone - - Server vlan ipv4, v6 PTR zone -- Operate Blocky as resolver and cache DNS - - blocky set the configurations with one code file - - It supports query routing based on its domain - Split horizon DNS - -## Consequences - -- Implementation of split horizon DNS -- ACME is available via nsupdate -- malicious DNS query is blocked in DNS level diff --git a/docs/adr/005-ids-ips.md b/docs/adr/005-ids-ips.md deleted file mode 100644 index 7392de5..0000000 --- a/docs/adr/005-ids-ips.md +++ /dev/null @@ -1,54 +0,0 @@ -# ADR 005 - IDS/IPS - -## Date - -- Feb/23/2026 - - First documentation - - -## Status - -- Accepted - -## Context - -- Automized detection and prevention threats via network - -## considerations - -### IPS -- Operate suricata as IDS and IPS - - Suricata IPS mode blocks packets by itself, bypassing nftables integration - - Suricata IPS mode overhead is very higher than IDS mode. - - Suricata IPS mode cannot detect and prevent TLS based communication. - - Homelab server resources are not enough to deal with high overhead. - -- fail2ban - - Single node only, no centralized decision sharing - - No community-based threat intelligence (CAPI) - - Regex based log parsing is less structured than CrowdSec's parser/scenario model - -- Crowdsec - - Community based rules and sinario (CAPI) - - Prevention based on local machines and parsers (LAPI) - - Bouncers can use nftables to prevent threats - - Parser can detect even L7 attack under TLS - -## Decisions - -- Operate suricata as IDS - - suricata IDS mode mirror all packets from interfaces - - match the packets based on its rules and writes log as fast.log, and eve.json - -- Operate Crowdsec as IPS - - CrowdSec uses two API server, CAPI, LAPI. - - CAPI updates malicious IPs based on community decisions - - LAPI decides malicious attack based on log from its parser and scenario (Suricata, caddy, etc) - - When CAPI, and LAPI decides block some IP based on log parsed by parser and scenarios, bouncer block the malicious accesses. - - Crowdsec register blacklist on nftables or iptables. - - -## Consequences - -- All malicious attack from WAN, even from LAN is controlled by CrowdSec and Suricata -- The firewall maintains high network throughput because blocking is performed efficiently at the OS network level (`nftables` sets) rather than through deep inline packet inspection. diff --git a/docs/adr/006-secrets.md b/docs/adr/006-secrets.md deleted file mode 100644 index d4226c1..0000000 --- a/docs/adr/006-secrets.md +++ /dev/null @@ -1,60 +0,0 @@ -# ADR 006 - Secrets - -## Date - -- Feb/23/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- Secret values must not uploaded anywhere as plain values. -- Manage secret values as Git without its real values. - -## Considerations - -### External KMS - -- HashiCorp Vault or Infisical - - Very powerful, but introduces significant compute/memory overhead. - - Creates a "Secret Zero" problem for a single-node homelab environment because of dependency (DB, or etc). - - It is hard to operate hardware separated key servers. - -### Systemd-credential - -- VM environment is hard to apply TPM for systemd-credential - - It is hard to guarantee the idempotency of TPM in virtual environment. - -### Ansible vault only - -- Ansible vault is powerful options but they are not convenient. - - It is necessary to encrypt separately outside of host_vars or group_vars' file. - - It is hard to add or modify secret values in inventory file. - -## Decisions - -- All secret data which has yaml format is encrypted by sops with age-key in `secret.yaml`. -- age-key is encrypted by gpg and ansible vault with master key (including upper, lower case, number, special letters) above 40 characters. - - All secret data always decrypt by `edit_secret.sh` script or ansible tasks from secrets.yaml using age-key encrypted by ansible-vault. - - decrypted secret data is always processed on ramfs, they are never saved on disk. -- Master key is never saved on disk, but only cold storage (USB, M-DISC, operators' memory) -- The secret data will be saved on each servers specific directory or podman secret. - - OS: - - path: /etc/secrets - owner: root:root - mode: 0711 - - path: /etc/secrets/\$UID - owner: \$UID:root - mode: 0500 - - Containers: - - podman secret: - path: /run/secret/\$SECRET_NAME - - These data are never backed up by kopia, or uploaded to git. - -## Consequences - -- Secret values are not located as a plain text in everywhere except where they are needed. -- It is possible to manage encrypted secret data with Git. diff --git a/docs/adr/007-backup.md b/docs/adr/007-backup.md deleted file mode 100644 index 264e565..0000000 --- a/docs/adr/007-backup.md +++ /dev/null @@ -1,77 +0,0 @@ -# ADR 007 - backup - -## Date - -- Feb/23/2026 - - First documentation - -- Feb/27/2026 - - Status changed from Deferred to Accepted - -- May/06/2026 - - Add backup checking rules - -## Status - -- Accepted - -## Context - -- All configuration file is managed by git (IaC) -- All data file should be backed up by kopia -- All backup should follow 3-2-1 backup cycle - -## Considerations - -### Backup Tool -- Restic / BorgBackup - - Also excellent deduplicating backup tools. - - However, Kopia provides a highly efficient native server mode (API) and cross-platform compatibility, making it easier to integrate with Synology DSM. - -### Database Backup Method -- Physical Backup (Raw data folder backup / File system snapshots) - - Backing up the `/var/lib/postgresql` directory directly while the DB is running can lead to severe data corruption and inconsistency. - - Logical dumps (`pg_dump`) are much safer, database-agnostic, and easier to restore in a homelab environment. - -### Silent failure problem - -- May/06/2026, the fact that backups haven't run since commit '9f236b6fa5' because of '%h' in system service unit. - - Operator couldn't realize backup doesn't run because the system service was failed silently. - - Therefore, set the checking rule. - - -## Decisions - -- All configuration files are managed by Git - - Configuration files are based on text - - It is necessary to version, history management. - - Local git -> private Gitea -> github private project (mirrored) - - This fulfills 3-2-1 backup rules -- Data files are managed by Kopia and DSM - - Local storage - kopia -> DSM's Kopia repository server - CloudSync -> Cloud server such as OneDrive or Google Drive - - This fulfills 3-2-1 backup rules -- Data files which needs backup - - DB data files: dump - - DB data files are located on infra:/home/infra/containers/postgresql/backups/{cluster,$service}/ - - App data files: Photos, Media, etc .. - - App data files are located on app:/home/app/data/ - - Backed up files: kopia - - DSM:/kopia/{infra,app}/ -- Kopia over DSM configuration is managed by runbook with equivalent CLI commands due to vendor limitation -- Restore will be processed manually - - DB data files - - From kopia server to console:$HOMELAB_PATH/data/volume/infra/postgresql/{cluster,data} - - APP data files - - From kopia server to APP vm after initiating before deploy services -- Automatic backup does not guarantee integrity of data system, so before reset the system conduct manual backup after making sure all services are shutdown. -- Check the repository once a week (Every monday) - - Check the snapshot in repository with `kopia snapshot list --all` - - Mount the snapshot respectively with `kopia mount $SNAPSHOT_ID $DESTINATION` - - Copy random file from snapshot and check the values. - - If there's some failure, check the backup service and conduct backup immediately. - - Repeat the check flow. - - When everything is done, umount the kopia mount with `ctrl+c` - -## Consequences - -- All files including configuration and data back ups will fulfill 3-2-1 (3 Copies, 2 different media, 1 offsite) back up rules diff --git a/docs/adr/008-passthrough.md b/docs/adr/008-passthrough.md deleted file mode 100644 index 66451fb..0000000 --- a/docs/adr/008-passthrough.md +++ /dev/null @@ -1,43 +0,0 @@ -# ADR 008 - passthrough - -## Date - -- Feb/23/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- App VM needs GPU for heavy workloads like Immich (hardware transcoding and machine learning) -- App VM needs huge data storage for its own services - -## Considerations - -### iGPU - -- SR-IOV - - SR-IOV is tech to divide PCIe devices for several VMs. - - Current stable linux kernel doesn't support sr-iov - - It is necessary to use DKMS for sr-iov - - Use DKMS is unstable depending on kernel upgrade, and the most important thing in server is stability. - - When passthrough iGPU itself, hypervisor cannot use graphic function. - - All nodes are managed by SSH session, so it is not a problem. - -### Storage - -- Each HDD - - Aoostar WTR Pro has their own sata controller for HDD. - - It is more effective and advantageous to passthrough SATA controller itself to manage btrfs RAID10, and HDD health check via S.M.A.R.T values. - -## Decisions - -- Passthrough N150's iGPU to APP VM -- Passthrough SATA controller to APP VM - -## Consequences - -- Passthrough iGPU itself to APP vm. -- Passthrough SATA controller to APP vm. diff --git a/docs/adr/009-isolation.md b/docs/adr/009-isolation.md deleted file mode 100644 index 633e9b6..0000000 --- a/docs/adr/009-isolation.md +++ /dev/null @@ -1,49 +0,0 @@ -# ADR 009 - isolation - -## Date - -- Mar/06/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- Distinguish borderline for service unit including hypervisor, vm, container - -## Considerations - -### Hypervisor - -- As a pure hypervisor, it should only operate virtualization for VM. -- Hypervisor just provides resources and dummy hub (br) - -### VM - -- VM should be distinguished based on their logical role. - - Firewall is responsible for networking - - Infra is responsible for infrastructure services such as DB, Monitoring, CA server - - Auth is responsible for authentication and authorization for services - - App is responsible for applications - -### Services - -- Services should be distinguished based on their needs (Privilege) - - Network stack, backup stack needs special privilege for low level ACL or networks. - - application stack doesn't need low level privilege usually - -## Decisions - -- Hypervisor: Only supply pure virtualization for VM -- VM: isolated by hypervisor from the other vms based on their role -- Services: - - the one which needs previlieges: Run as native on vm. Don't make overhead for virtualization. - - the one which doesn't need previlieges: Isolate as container from host. - -## Consequences - -- Guarantee scurity integrity -- Simple operational rules -- Optimize the limited resources \ No newline at end of file diff --git a/docs/adr/010-provisioning.md b/docs/adr/010-provisioning.md deleted file mode 100644 index f9c8ab8..0000000 --- a/docs/adr/010-provisioning.md +++ /dev/null @@ -1,35 +0,0 @@ -# ADR 010 - provisioning - -## Date - -- Mar/06/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- Every sensitive process should be controlled and managed. - -## Considerations - -### Automate destroying process - -- Destroying is not frequent process, there's no reason to make complex logic -- Sensitive process should be double checked by human manually - -## Decisions - -- Make provisioning process as auto -- Make sensitive process as manual - - Removing - - Formatting - - Destroying - - Certificates and CA ([ADR-003](./003-pki.md)) - - Etc. what operator decides that is sensitive - -## Consequences - -- All process can be under control of the operator diff --git a/docs/adr/011-tls-communication.md b/docs/adr/011-tls-communication.md deleted file mode 100644 index da515be..0000000 --- a/docs/adr/011-tls-communication.md +++ /dev/null @@ -1,33 +0,0 @@ -# ADR 011 - TLS communication - -## Date - -- Mar/06/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- To make administrational policy simple -- Set the principle of TLS communication boundry - -## Considerations - -### Apply mTLS - -- implementing mTLS needs both client certificate and server certificate -- Managing a number of certificates makes a huge operational burden (expiry date, revocation, etc ..) - -## Decisions - -- Set TLS for all communication except 'lo' interface -- When it is possible to activate TLS, apply it even in 'lo' interface - -## Consequences - -- The policy is set simple -- The overhead is increased little -- Exclude the exceptions on operation (For the administrator) diff --git a/docs/adr/012-alerting.md b/docs/adr/012-alerting.md deleted file mode 100644 index 242c893..0000000 --- a/docs/adr/012-alerting.md +++ /dev/null @@ -1,45 +0,0 @@ -# ADR 012 - Alerting - -## Date - -- Mar/08/2026 - - First documentation - -## Status - -- Accepted - -## Context - -- The necessity of observability -- Difficulty of realizing present status of services -- Stable restoring process already exists - -## Considerations - -### Mail based - -- MTA is hard to manage even when operator uses this as relay host -- The mail protocol is complex to implement only for internal mail system for single operator - -### Chat based - -- Using discord, telegram is easy to get announcment automatically -- The dependency of external services - -## Decisions - -- Do not operate alerting system - - Single node system for small group doesn't need HA - - When single node system is down, the alerting system is also down. -- When the alert system is needed, implement the system on free instance of external IaaS like AWS or Azure - -## Consequences - -- Simple management and stable restoring - - Check service availability - - Check from grafana - - Access to node via vpn with ssh - - Access to node via physical VLAN - - Reprovisioning the node -- The additional possibility of extension with Cloud services. diff --git a/docs/archives/2025-06.tar.gz b/docs/archives/2025-06.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..21b9b06333312091e6a9716890b8f89b648ba20c GIT binary patch literal 75524 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�Yu&p=eIrSb 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 literal 0 HcmV?d00001 diff --git a/docs/archives/2025-06/on-premise.txt b/docs/archives/2025-06/on-premise.txt deleted file mode 100644 index a72c25f..0000000 --- a/docs/archives/2025-06/on-premise.txt +++ /dev/null @@ -1,12151 +0,0 @@ -개인 활용 목적의 On-premise 홈 네트워크 환경 구축 프로젝트 -이 프로젝트의 목표는 개인이 활용하기 위한 On-premise 홈 네트워크 환경을 집에서 사용 가능한 장비와 네트워크 구조로 구현하면서 다양한 기술 요소(네트워크, OS, 가상화와 컨테이너, 자원 관리, 데이터 베이스, 상용 서비스 운영, 보안)이 네트워크 상에서 어떻게 하나의 시스템을 이루고 작동하는지에 대하여 깊이 이해하고, 이를 체득하는 것에 있습니다. - -1. 프로젝트 개요 - -1.1. 동기 및 목표 -동기 - - 본 프로젝트의 시작은 개인적인 사진 및 데이터를 저장하기 위한 방법을 찾는 것이었습니다. 처음 고려한 방법은 클라우드 서비스를 사용하는 것이었습니다. 하지만 클라우드를 사용하는 방식은 클라우드 업체에서 소유한 데이터에 언제나 접근할 수 있다는 위험을 가지고 있습니다. 이는 소유한 데이터에 대하여 완전한 통제가 불가능함과 동시에 온전한 권리의 부재를 의미합니다. 따라서 이러한 문제를 해결하기 위해 처음 선택한 방식은 NAS(Network Attached Storage)를 구축하는 것이었습니다. 하지만 NAS를 구축한 이후 이 방식 역시 설정과 사용에 있어 추상화된 부분이 많아 해당 문제에서 자유로울 수 없었습니다. 데이터를 사용하며 자유롭게 설정하고 다루지 못한다면 이는 클라우드 환경과 다를 것 없거나 혹은 더 위험할 수 있다 생각했습니다. - - 이에 단순한 NAS 구성을 벗어나기로 결정하였습니다. 개인 데이터의 모든 권한과 책임을 가지고 사용 가능한 On-premise 환경을 구축한다는 목표를 세웠습니다. 개인이 구현한 On-premise 환경이 상용 서비스 수준의 가용성과 안정성을 담보하기는 힘들 것입니다. 하지만 본 프로젝트를 통하여 직접 On-premise 환경을 구축하면서 현대 IT(Information Technology) 인프라의 필수 구성요소들과 상호작용, 또 이러한 환경 자체에 대한 이해와 경험을 하고자 합니다. 이를 통해 IT 서비스와 네트워크 환경을 이용함에 있어 스스로 가장 약한 고리가 되지 않는 것 역시 목표입니다. - - 과거 군 복무를 하며 분산된 지역망과 삼중화된 물리적 HA(High Availability)를 갖춘 대규모 On-premise 환경의 전장망을 관리하였습니다. 당시의 경험 속 이해하기 힘들었던 복잡한 매뉴얼과 SoC(Separation of Concerns)에 맞춰 설계된 구조들, LTO(Linear Tape Open) 기반의 자동 백업 시스템, 체계적인 복구 시나리오를 직접 운영했던 경험을 본 프로젝트에 적용하려 합니다. 나아가 단순히 명령어를 입력하고 매뉴얼 중심의 운용을 했던 과거의 경험에서 벗어나 아래와 같이 목표를 설정하고 이를 성취하려 합니다. - - 목표 - - 구현 목표 - - 대상 - - - - 2~5명의 소규모 인원 - - - - 요구 기능 - - - - IT 서비스 제공을 위하여 다음과 같은 기능을 가진 서버 및 네트워크 운영 - - - - 방화벽 - - Local CA(Certificate Authority) - - Local DNS(Domain Name System) - - Proxy - - DB(Database) - - - - - - 파일, 미디어, 어플리케이션 서버 등의 개인 클라우드 환경 구축 - - 블로그, wiki 등의 웹 서비스 호스팅 - - IPS/IDS(Intrusion Prevention/Detection System), Kali 등을 통한 정보보안 실습 환경 제공 - - 단, 다음 기능들은 상용 서비스급 가용성 및 기능을 확보할 필요가 없으며, 학습 및 경험을 우선 - - - - 시스템 아키텍쳐 설계 및 기술 통합 - - 제한된 물리적 환경 및 하드웨어/네트워크 제약 사항 속에서 주어진 요구 사항을 최선의 방식으로 구현합니다. 필수 요구 사항을 구현하며 과도한 투자를 지양하고 효율적인 자원 배분을 하는 능력을 기르고자 합니다. 또한, 구현 전 발생 가능한 문제점을 사전에 발견하고, 이러한 문제점을 해결하기 위하여 어떤 기술을 선택하고 조합하여 설계하고 구현할 것인지에 대한 능력을 기르고자 합니다. - - 추상적 개념의 구체화 및 실무 적용 - - OS(Operating System), 네트워크, 정보보안 등 여러 분야 속 추상적이고 개별적인 개념들을 조합하여 실제로 구현합니다. 이 과정 속에서 각 개념들의 연결과 동작 원리를 자세하게 파악합니다. 이를 통해 이론적인 지식을 어떻게 현실에서 구현하고 적용할 수 있는지에 대한 감각을 익히려 합니다. 모든 기술 스택을 전부 직접 구현하는 것이 아닌 인프라의 핵심 요소와 통제하에 위임 가능한 요소를 분리합니다. 필수적인 부분에 집중하여 주도권 있는 위임을 통해 프로젝트 전반의 기술 품질과 유지 관리성을 높이는 분업 환경을 구현하고자 합니다. - - 문제 해결 및 제약 조건의 극복 - - 프로젝트를 진행하면서 사전에 고려하지 못한 다양한 기술적, 환경적 문제들과 마주할 것입니다. 이러한 문제들이 발생하였을 때 당황하지 않고 정확하게 분석하고 기술적인 근거에 기반하여 해결책을 찾아 적용하는 능력을 기르고자 합니다. - - 체계적인 문서화 및 지식 관리 - - 문서화는 모든 프로젝트 진행에 있어 가장 큰 의미를 갖습니다. 프로젝트 진행 중 모든 과정(계획, 설계 및 근거, 구현 절차 및 로그, 설정, 문제 해결 사항에 대한 기록 등)을 BookStack을 활용하여 체계적으로 문서화를 함으로 단순한 구축 경험을 넘어 지식과 경험을 내재화 하고 관리 및 공유 가능한 자산으로 만드는 능력을 기르고자 합니다. - - 결론 - - 본 프로젝트는 처음부터 완벽한 인프라 설계와 오류 및 시행 착오 없는 구축을 목표로 하지 않습니다. 프로젝트를 진행하며 얻고자 하는 궁극적인 가치는 위와 같은 목표를 가지고 단순하게 개인이 사용 가능한 IT 환경을 구축하는 것과 기술을 배우는 것에 그치지 않습니다. 프로젝트 자체와 이를 통해 경험하는 실패와 시행 착오 역시 훌륭한 학습이 될 것입니다. 특히 문제 해결을 위한 사고력과 실행력을 기르고 작은 범위에서라도 안정적인 서비스를 설계 통제 하며 이를 기반으로 안정적인 서비스 운영을 할 수 있는 수준에 도달하는 것이 궁극적인 가치이자 목표입니다. - - - - 2025-05-13 - 초안 작성 - - 2025-05-15 - 태그 수정 및 동기와 결론, 오타 수정 - - 2025-05-16 - 동기와 결론 수정 - - 2025-05-29 - 결론 수정/요구 기능 제약 조건 구체화 - - 2025-06-14 - 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-20 - 날짜 표기 변경 - -1.2. 하드웨어 구성 -목표 - - 프로젝트를 진행하기 위하여 구매한 하드웨어를 목록으로 정리합니다. - - 하드웨어 구성 - - 라우터/L2 스위치 - - 메인 라우터(ipTime T5004; T5004) - - - - 가격 42,900 KRW - - 외부 Gateway 역할 수행 - - 메인 DHCP(Dynamic Host Configuration Protocol) 서버 역할 수행 - - EasyMesh controller 역할 수행 - - 1차 방화벽 기능 수행 - - VLAN 기능 미지원 - - OpenWRT 지원 기종으로 추후 프로젝트시 VLAN(Virtual Local Area Network; 802.1Q) 기능 지원 기기로 사용 가능 - - - - 서브 라우터(ipTime AX6000M; AX6000M) - - - - 가격 99,000 KRW - - 무선 AP(Access Point) - - EasyMesh node 역할 수행 - - VLAN 기능 미지원 - - - - 스위치(ipTime H8005G-IGMP; H8005G) - - - - 가격 17,900 KRW - - 5포트 L2 스위치 - - VLAN 기능 미지원 - - 추후 OpenWRT 업데이트 프로젝트 진행 시 T5004로 변경 예정 - - - - 서버 - - 서버 하드웨어(Aoostar WTR Pro N150; WTR Pro) - - - - 가격 254,900 KRW - - Intel N150 4Core/4Thread/TDP 6W - - DDR4 SO-DIMM slot x1 - - M.2 NVMe slot x1 - - SATA 3.5" slot x4 - - 2.5Gbps NIC(Network Interface Card) x2 - - Hypervisor OS 설치를 통해 여러 역할을 수행하는 VM 및 LXC가 논리적으로 격리되어 각 역할 수행 - - - - RAM(Samsung DDR4 SO-DIMM 32GB) - - - - 가격 106,900 KRW - - - - SSD(Samsung M.2 NVMe SSD 1TB 980PRO) - - - - 기존 사용하던 SSD로 프로젝트를 위하여 따로 구매 하지 않음 - - 가격 276,000 KRW(2022/04/22 기준, 현재 최저가 184,000 KWR) - - - - HDD(Seagate/HGST 3.5" HDD 2TB x4) - - - - 가격 200,000 KRW - - SATA 6Gbps - - - - 외부 백업 서버 - - 서버 하드웨어(Synology DS124; DS124) - - - - 가격 233,000KRW - - Realtek RTD1619B 4Core/4Thread - - DDR4 1GB RAM x1 - - SATA 3.5" x1 - - 1Gbps NIC x1 - - 홈 서버가 구축되기 전 까지 홈 서버 역할 수행 - - 홈 서버 구축 이후로는 홈 서버의 외부 백업 서버 역할 수행 - - - - HDD(Toshiba 3.5" HDD 4TB) - - - - 가격 55,000KRW - - 4TB SATA 3.3Gbps - - - - - - 2025-05-13 - 초안 작성, 각 서버의 역할 추가 - - 2025-05-15 - 태그 수정 - - 2025-05-25 - 네트워크 장비 VLAN 미지원 명시 - - 2025-05-29 - SSD 변경(256GB Hynix > 1TB Samsung) - - 2025-05-30 - T5004가 OpenWRT를 지원하는 사실 명시 - - 2025-06-14 - 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-20 - 날짜 표기 변경 - -1.3. 목표 아키텍처 -목표 - - - - 현재 집안의 물리적인 구조를 파악합니다. - - 프로젝트를 통해 구현하고자 하는 서비스를 포함한 목표 아키텍처를 정의합니다. - - - - 목표 아키텍처 구조 - - 물리 구조도 - - - - 물리적 네트워크 구조 설명 - - - - WAN(Wide Area Network) - ISP(Internet Service Provider) Modem - T5004로 WAN과 LAN(Local Area Network)이 구별 - - 통신 단자함에는 전원부가 존재하지 않으며 PoE(Power over Ethernet; 802.3af, 802.3at, 802.3bt)로 전원을 공급 - - T5004는 LAN-WAN 통신의 메인 Gateway로 DHCP 서버 역할 수행, DHCP Relay 및 VLAN 지원하지 않음 - - AX6000M은 AP로 허브 모드로 작동하며, 외부 백업 서버(NAS)와 연결, VLAN 지원하지 않음 - - H5008G는 L2 허브로 WTR Pro의 NIC0, NIC1과 연결, VLAN 지원하지 않음 - - - - 집안의 물리적인 제약 사항들로 인하여 통신 단자함에 새로운 기기 추가의 어려움이 있습니다. 또한 WTR Pro 외부 클라이언트에 대하여 자유로운 VLAN 설정이 불가능합니다. - - 아키텍처 구조도 - - - - 아키텍처 구조 설명 - - 사용자 - - 2~5명의 소규모 인원 - - 네트워크 - - WAN - - - - Public DNS를 통하여 들어오는 Well-Known DNS 패킷만을 허용(DDoS; Distributed Denial of Service 방지) - - Public IP(Internet Protocol) adress를 통한 직접 접속 차단(Port Scan 방지) - - https(443) 및 VPN(Virtual Private Network) 포트만 외부 개방 - - WAN에서 T5004로 접속 차단, UPnP(Universal Plug and Play) 등의 취약 기능 전체 차단, 강력한 비밀번호 사용, 최신 펌웨어 유지 - - Cloudflare API를 통한 DDNS(Dynamic Domain Name System) 자동화 - - Cloudflare를 통한 공인 SSL/TLS(Secure Sockets Layer/Transport Layer Security) 인증서 발급 및 자동 갱신 - - T5004는 Untagged VLAN을 위한 DHCP 서버 및 외부 Gateway 역할 수행 - - - - LAN - - - - OPNsense가 LAN(VLAN 포함)의 내부 라우터로 작동 - - - - WTR Pro의 NIC0과 NIC1의 역할을 분리 - - NIC0은 vmbr0, NIC1은 vmbr1 할당 - - - - vmbr0: OPNsense의 vtnet0과(T5004와 연결되어 WAN 통신) - - vmbr1: OPNsense의 vtnet1과 연결(Trunk mode, Untagged VLAN, VLAN2,3,4,10 연결되어 LAN 통신, PVE Web UI 연결) - - - - - - 모든 LAN(VLAN 포함)의 기본 Gateway는 OPNsense(T5004의 DHCP 서버 기본 Gateway 포함) - - OPNsense의 Gateway는 T5004로 고정 할당 - - T5004는 NAT가 허용된 모든 패킷을 OPNsense로 포트 포워딩 - - - - - - FreeIPA를 통한 LDAP(Lightweight Directory Access Protocol)/SSO(Single Sign ON) 및 Kerberos, Local CA, Local DNS(Split Horizon DNS) 구현 - - AdGuard Home LXC를 통하여 광고 차단 필터를 적용한 DNS 서비스 구현 - - - - AdGuard Home > FreeIPA DNS > Cloudflare DNS(DoH; DNS over Https 사용) - - - - - - Zero Trust 원칙에 따라 클라이언트 간 iptables 등 Local 방화벽 설정을 통한 통신 제어 강화 - - PVE(Proxmox Virtual Environment) 내부의 vmbr(Linux Bridge) 기능을 활용하여 VM(Virtual Machine)/LXC(LinuX Container)/Docker를 OPNsense와 연결 - - 보안 실습을 위한 Kali와 실습 Client는 별도의 VLAN을 통하여 격리 - - 각 서비스 별 VLAN을 나누어 IP 할당 - - - - Untagged VLAN(192.168.0.0/24): Untagged VLAN으로 외부 클라이언트 망(PC, DS124, Mobile Devices) - - - - 주요 Clients(PC, DS124 등) Static IP 할당 - - Mobile Devices 및 IoT 기기는 Dynamic IP 할당 - - - - - - VLAN2(192.168.2.0/24): PVE 내부 서버 및 서비스망 - - VLAN3(192.168.3.0/24): Kali 망 - - VLAN4(192.168.4.0/24): 정보보안 실습 Client 망 - - VLAN10(10.0.0.0/24): VPN Clients 망 - - - - - - VLAN 간 통신 제어 - - - - VLAN 간 통신시 접근 가능 IP 제한 - - 외부 통신시 VPN 이용한 접근만 가능 - - 특정 서비스(Reverse Proxy 등)은 서비스 목적에 따라 필요한 범위 내에서 접근 허용 - - - - - - - - WTR Pro - - 기본 사항 - - - - PVE를 통하여 여러 기능의 서버를 중요도와 특성에 맞게 각각 VM 및 LXC로 논리적 분리 구축 - - 모든 LXC는 기본적으로 root 권한이 포함되지 않은 Unprivileged LXC로 구축 - - LXC 상에서 Host의 Root 권한이 필요한 Docker 서비스를 운영하지 않음(Docker 사용 시 VM 활용) - - NIC0, NIC1은 OPNsense에 vtnet으로 할당 - - - - NIC0: WAN 통신 담당 - - NIC1: LAN(VLAN) 통신 담당 - - - - - - 각 서비스 별 대표 UID/GID Matrix를 작성(FreeIPA LDAP/SSO 전용 UID 대역과 분리) - - OPNsense 및 FreeIPA가 SPOF(Single Point of Failure)가 되므로 자동 복구 절차 및 수동 복구 절차 수립(자동화 모니터링/복구 스크립트 작성 후 활용) - - 각 VM과 LXC에 지정된 리소스는 모니터링 하며 세부 조정 - - - - 로깅 및 모니터링과 서버 설정 - - - - 로깅 및 모니터링 - - - - Prometheus와 Grafana를 통한 모니터링 및 시각화 - - Loki와 Promtail을 활용한 log 중앙 집중 - - Alertmanager를 통한 특이 log 알림 - - - - - - Ansible + Semaphore(Web UI)를 활용한 서버 설정 자동화 - - Gitea를 활용한 설정 파일 아카이빙 - - 추후 n8n과 연동하여 모든 과정 자동화 - - - - 파일 및 백업 - - - - Btrfs(B-Tree File System)의 CoW(Copy on Write) 기능으로 인한 외부 단편화 방지를 위하여 파일 서버 상 3개월 마다 한 번씩 btrfs filesystem defragment 명령어 수행 - -  시스템 - - - - Btrfs 스냅샷을 활용하여 하루 1회, 15일간 스냅샷 보관 - - OS 파티션 및 VM/LXC 저장소 한 달 1회 백업(설정 변경 시 이상 없는 경우 즉시 백업)하여 파일 서버에 저장, 최근 3개 버전 유지 - - - - - -  어플리케이션 - - - - OPNsense/FreeIPA 설정, DB, docker-compose.yml 등의 주요 설정 및 데이터 백업하여 Gitea에 저장 - - 각 서버의 로그 보관 기간 7일, 이후 파일 서버로 이관 후 3 개월 보관 후 파기(일반 삭제) - - - - - - 데이터 - - - - 파일 서버의 데이터(NFS 공유 데이터 및 백업 데이터)를 DS124 및 클라우드에 백업 (Kopia를 통한 백업 기능 활용, E2EE 기능 적용) - - 추후 데이터 용량이 커지면 Amazon Glacier 서비스 이용 고려 - - - - - - 실제 환경 구축 후 더욱 구체적이고 실질적인 백업 전략 수립 - - - - 필요 서비스(VM/LXC/Docker) - - - - OPNsense VM - - Rocky Linux - FreeIPA VM - - AdGuard Home LXC - - Monitoring Server LXC - - Proxy Server LXC - - File Server VM - Docker - - - - Portainer Agent - 도커 통합 관리 - - Seafile - 파일 관리 및 동기화 - - Kopia - 백업 및 아카이빙 - - - - - - DB Server VM - Docker - - - - Portainer Agent - 도커 통합 관리 - - Maria DB/Postgre SQL - DBMS - - - - - - Web Server/WAS VM - Docker - - - - Portainer Agent - 도커 통합 관리 - - Homepage - 홈페이지 - - Ghost - 블로그 - - Bookstack - wiki, 노트 - - Gitea - Git 버전 관리 - - - - - - Application Server VM - Docker - - - - Portainer Server - 도커 통합 관리 - - Uptime Kuma - 도커 서비스  모니터링 - - Diun - 도커 업데이트 관리 - - n8n - 워크 플로우 자동화 - - Redis - 캐시 DB - - Code-Server - 웹 코드 에디터 - - Immich - 사진 및 동영상 관리 - - PeerTube - 동영상 컨텐츠 관리 - - Navidrome - 음악 관리 - - Vaultwarden - 사용자 암호 관리 - - Infisical - DB, API key 등 관리 - - Matrix-Synapse/element - 채팅/통화/영상 통화 - - Radicale - 캘린더 및 주소록 - - Vikunja - To-Do 리스트 - - Firefly III - 가계부 - - Paperless-ngx - 문서 관리 - - Snipe-IT - ITAM; IT 자산 관리(하드웨어, 소프트웨어 라이센스 등) - - Calibre-web - 전자책 관리 - - Kmoga - 만화책 관리 - - Audiobookshelf - 오디오북 관리 - - - - - - Kali and practice client VM - - - - 가용성 확보를 위한 리소스 및 복구 우선 순위 - - - - 네트워크 및 인증 핵심 기능을 우선 복구하여 서비스 의존성 문제 최소화 - - PVE > OPNsense > FreeIPA > Proxy Server > DB Server > File Server > etc - - - - DS124 - - - - 구축 초기에는 Docker 등의 필요 서비스 임시 호스팅 - - 구축 중반에는 초기 역할을 마이그레이션 하서 백업 서버 역할 수행 - - 구축 이후에는 완전한 백업 서버로 역할 전환 - - - - 결론 - - 해당 목표 아키텍처는 프로젝트가 진행되며 변경될 수 있습니다. 목표 아키텍처를 계속해서 발전시키며 구현해 나갈 것입니다. - - - - 2025-05-13 - 초안 작성 - - 2025-05-15 - 태그 수정 및 최종 목표 아키텍처 수정, 이상적인 아키텍쳐 항목 작성 - - 2025-05-24 - 최종 목표 아키텍쳐 및 이상적인 구조의 아키텍쳐 구조도 수정 - - 2025-05-25 - 최종 목표 아키텍쳐 및 이상적인 구조의 아키텍쳐 구조도 수정/VLAN, vtnet, vmbr 관련 내용 추가 - - 2025-05-28 - DB server LXC를 VM으로 변경/로깅 및 모니터링 전략 추가/ 백업 전략 구체화 - - 2025-05-29 - T5004 보안 대책 추가/Ansible을 통한 설정 자동화 및 조각모음 대책 추가 - - 2025-05-30 - 네트워크 설계 VLAN 반영 수정/OpenWRT 적용 시 VLAN, HA 가능 구조 적용 - - 2025-05-31 - VLAN/HA 추가 구조를 1.4. 추가 프로젝트 계획 으로 옮김 - - 2025-06-02 - vtnet0과 vtnet1의 역할 명확화 - - 2025-06-15 - 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-16 - AdGuard Home LXC 내용 추가 - - 2025-06-19 - VPN Clients 망 추가 - - 2025-06-20 - 날짜 표기 변경 - - 2025-06-23 - VLAN 표기 방식 수정 - -1.4. 추가 프로젝트 계획 -목표 - - 프로젝트의 목표 아키텍처 구현한 이후 이어서 진행할 추가 프로젝트의 청사진을 정의합니다. - - OpenWRT 적용 계획 - - 목표 - - 현 프로젝트에서 메인 라우터인 T5004는 VLAN을 지원하지 않아 WTR Pro 외부 클라이언트에 대한 VLAN 구성이 어렵습니다. 이때 T5004는 오픈 소스 라우터 OS인 OpenWRT를 설치할 수 있습니다(비고: OpenWRT 지원 목록 - ipTime T5004 ). 따라서 T5004에 OpenWRT를 설치 시 WTR Pro 외부의 클라이언트 또한 VLAN으로 L3 망 격리가 가능해져 보다 정밀한 네트워크 관리가 가능합니다. - - OpenWRT 적용시 구조 및 아키텍처 - - 현 프로젝트의 목표 아키텍처를 구현한 후 안정적인 운영을 할 수 있을 때 T5004에 OpenWRT를 설치하여 구현하고자 하는 아키텍처입니다. - - 물리 구조도 - - - - 물리적 네트워크 구조 설명 (기존 구조에서 수정된 부분) - - - - WAN - ISP Modem - T5004(OpenWRT) - T5004(OpenWRT) - OPNsense 구조 - - - - 각 방의 랜 포트가 하나 뿐이고, 회선도 하나 뿐인 물리적 한계로 인한 타협사항 - - 두 장비 모두 L2 관리형 스위치로 연결된 포트에 VLAN10(WAN Paththrough 망)을 태깅 - - 이 계획에서는 WAN Paththrough(VLAN을 통한 WAN 트래픽 격리 전달)를 통해, OPNsense는 공인 IP 그대로 전달 받음 - - - - - - WTR Pro가 주 라우터 및 IPS/IDS로 작동 - - AX6000M은 무선 AP로 허브 모드로 작동하며, 외부 백업 서버(NAS)와 연결, VLAN 지원하지 않음 - - H5008G가 T5004(OpenWRT)로 대체 - - - - 아키텍처 구조도 (기존 구조에서 수정된 부분) - - - - - - 아키텍처 구조 설명 (기존 아키텍처에서 수정된 부분) - - 사용자 - - 2~5명 - - 네트워크 - - - - WAN - - - - 외부 DNS를 통한 Well-Known DNS의 패킷만을 허용(DDoS 방지/ OPNsense에서 처리) - - T5004는 WAN Paththrough가 가능한 L2 관리형 스위치로 작동 - - 모든 라우팅 및 IPS/IDS, Gateway는 OPNsense에서 처리 - - - - - - LAN - - - - VLAN을 통한 엄격한 내부 망분리(DMZ는 관제/CERT 운영 없이는 비효율적) - - 각 장비 별(네트워크 장비, 서버, 주요 클라이언트) 등에 대한 고정 IP 설정 - - - - 관리할 고정 IP가 적으므로, MAC기반 DHCP 예약이 아닌 IP 매트릭스를 운영하며 수동으로 정의  - - VLAN100(192.168.10.0/24 ): WAN Paththrough 망 - - VLAN2(192.168.2.0/24): 클라이언트(PC 등) 망 - - VLAN3(192.168.3.0/24): Hypervisor 내부 서버 및 서비스 망 - - VLAN4(192.168.4.0/24): Mobile Devices 및 Guest 망 - - VLAN5(192.168.5.0/24): Kali 망 - - VLAN6(192.168.6.0/24): Kali를 통한 정보보안 실습망 - - VLAN10(10.0.0.0/24): VPN Clients 망 - - - - - - - - - - - - - - HA를 위한 계획 - - 목표 - - 필요 선행 프로젝트: OpenWRT 적용 - - 현 프로젝트의 목표는 상용 서비스 만큼의 가용성을 확보하는 것은 아닙니다. 이는 많은 시간과 자원을 투자하여야 하여야 하며, 현실적인 제약 조건을 고려하여야 합니다. 하지만 HA는 현대 IT 인프라의 중요한 요소로 이를 경험 및 학습을 하는 것은 큰 의미가 있습니다. 따라서 현재 가장 큰 SPOF인 OPNsense와 FreeIPA를 HA 구성하여 실제 작동을 경험하고 테스트 해보고자 합니다.  - - HA 적용시 구조 및 아키텍처 - - 물리 구조도 - - - - - - 물리적 네트워크 구조 설명 (OpenWRT 계획 구조에서 수정된 부분) - - - - - - WAN - ISP Modem - T5004(OpenWRT) - T5004(OpenWRT) - OPNsense 구조 - - - - 각 방의 랜 포트가 하나 뿐이고, 회선도 하나 뿐인 물리적 한계로 인한 타협사항 - - 두 장비 모두 L2 관리형 스위치로 연결된 포트에 VLAN100(WAN Paththrough 망)을 태깅 - - 이 계획에서는 WAN Paththrough(VLAN을 통한 WAN 트래픽 격리 전달)를 통해, OPNsense는 공인 IP 그대로 전달 받음 - - - - - - Mini PC가 주 라우터 및 IPS/IDS로 작동 - - AX6000M은 무선 AP로 허브 모드로 작동하며, 외부 백업 서버(NAS)와 연결, VLAN 지원하지 않음 - - H5008G가 T5004(OpenWRT)로 대체 - - - - 아키텍처 구조도 - - - - 아키텍처 구조 설명 (OpenWRT 계획 구조에서 수정된 부분) - - - - 사용자 - - 2~5명 - - 네트워크 - - - - WAN - - - - 외부 DNS를 통한 Well-Known DNS의 패킷만을 허용(DDoS 방지/ OPNsense에서 처리) - - T5004는 WAN Paththrough가 가능한 L2 관리형 스위치로 작동 - - 모든 라우팅 및 IPS/IDS, Gateway는 OPNsense main에서 처리(OPNsense HA도 stanby로 설정) - - - - - - LAN - - - - VLAN을 통한 엄격한 내부 망분리(DMZ는 관제/CERT 운영 없이는 비효율적) - - 각 OPNsense의 vtnet0은 Trunk로 WAN(VLAN100) 및 HA LAN(VLAN200)과 연결 - - 각 OPNsense의 vtnet1은 Trunk로 VLAN(VLAN2, 3, 4, 5, 6, 10) 과 연결 - - 각 장비 별(네트워크 장비, 서버, 주요 클라이언트) 등에 대한 고정 IP 설정 - - - - 관리할 고정 IP가 적으므로, MAC기반 DHCP 예약이 아닌 IP 매트릭스를 운영하며 수동으로 정의 - - - - VLAN100(192.168.10.0/24 ): WAN Paththrough 망 - - VLAN200(192.168.20.0/24): HA 구성 및 관리 망 - - VLAN2(192.168.2.0/24): 클라이언트(PC 등) 망 - - VLAN3(192.168.3.0/24): Hypervisor 내부 서버 및 서비스 망 - - VLAN4(192.168.4.0/24): Mobile Devices 및 Guest 망 - - VLAN5(192.168.5.0/24): Kali 망 - - VLAN6(192.168.6.0/24): Kali를 통한 정보보안 실습망 - - VLAN10(10.0.0.0/24): VPN Clients 망 - - - - - - - - - - - - - - - - WTR Pro, mini PC - - - - 기본 사항 - - - - OPNsense 및 FreeIPA가 SPOF(단일 실패 지점)가 될 수 있으므로 물리적인 HA 구성 - - 각 서버의 리소스 부하를 고려하여 메인 서버를 mini PC, 스탠바이 서버를 WTR Pro에 구성 - - 각 VM 및 LXC에 지정된 리소스는 모니터링 하면서 유동적으로 변경 - - - - - - 필요 서비스(VM/LXC/Docker) - - - - OPNsense VM(HA 구성) - - FreeIPA VM(HA 구성) - - AdGuard Home LXC - - Monitoring Server LXC - - Proxy Server LXC(HA 구성) - - File Server VM - - DB Server VM - - Web Server/WAS VM - - Application Server VM - - Kali and practice client VM - - - - - - 가용성 확보를 위한 복구 우선 순위 - - - - 네트워크 및 인증 핵심 기능을 우선 복구하여 서비스 의존성 문제 최소화 - - PVE > OPNsense > FreeIPA >Proxy Server > DB Server > File Server > etc - - - - - - - - - - On-premise 환경 규범집 작성 및 정보보안 법률 적용 계획 - - 목표 - - 단순한 서버 구현 및 서비스 제공을 넘어서, 이 서비스를 안전하게 운영하기 위한 첫 걸음을 다음 추가 프로젝트를 통해 진행하려 합니다. 개인 On-premise 환경을 위한 규범집을 작성하고, 법률을 서비스 환경에 적용하여 ISMS-P 기준을 간략하게 적용하여 간이 보고서를 작성하려 합니다. 이는 보안이 실 서비스 운영에 있어서 얼마나 중요한지 확인하고, 법과 규정을 실제로 서비스에서 왜 필요한지에 대한 심도 싶은 이해를 하고자 합니다. - - - - IPv4, IPv6 동시 서비스 계획 - - 목표 - - 현대 인터넷 환경은 IPv4 체계의 주소 고갈과 한계로 인하여 IPv6 체계의 도입하고 있는 과도기적 상황입니다. 이러한 상황 속에서 IPv4의 NAT(Network Address Translation), Subnetting, Broadcast와 Unicast, IPv4 환경에서 네트워크 보안 등에 대하여 심도 깊은 이해를 하고, IPv6 환경에서 어떻게 같은 서비스를 지속할 수 있을지에 대한 학습 및 경험을 하고자 합니다. - - - - IaaS(Infrastructure as a Service) 등 외부 서비스와 연동 계획 - - 목표 - - 현대 IT 인프라는 절대 On-premise 환경에서만 작동하지 않습니다. 수많은 서비스와 On-premise 환경이 동시에 공존하며 상호 보완적인 관계를 띄고 있습니다. 이에 On-premise 환경 구축 후 이를 Azure와 같은 외부 IaaS 서비스와 직접 연동을 하는 프로젝트를 통해 현대 IT 인프라가 어떤 형태로 작동하는 지에 대한 더욱 깊은 이해를 추구합니다. - - - - 2025-05-24 - 초안 작성 - - 2025-05-28 - IPv4, IPv6 동시 서비스 계획 추가 작성 - - 2025-05-29 - IaaS 등 외부 서비스와 연동 계획 추가 작성 - - 2025-05-30 - HA 계획에 OpenWRT 내용 추가 - - 2025-06-16 - OpenWRT와 HA 계획 분리 및 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-19 - VPN Clients 망 추가 - - 2025-06-20 - 날짜 표기 변경 - -2. 프로젝트 설계 - -2.1. 하드웨어 선정 -목표 - - - - 본 프로젝트의 요구 사항을 만족시킬 수 있는 하드웨어를 선정합니다. - - 선택된 하드웨어 (WTR Pro N150 및 상세 사양)이 성능 한계와 기대치를 확인합니다. - - 각 과정의 고려사항 및 근거를 명확하게 기록합니다. - - - - 요구 사항 - - - - 실사용 및 학습 목적에 성능 요구사항을 충족해야 합니다. - - - - 다중 VM/LXC 환경 - - IPS/IDS 학습 목적 운용(통제된 Inbound 트래픽 및 학습 목적의 최소 운영) - - 소규모 사용자를 위한 Web, Media, Application Service 제공(트랜스코딩 및 동시 부하 가능성 매우 낮음) - - - - - - 24/7 운영을 전제로 하고 있으므로 다음과 같은 조건을 만족해야 합니다. - - - - 저전력 - - 저발열 - - 저소음 - - 안정성 - - - - - - 다음과 같은 필수 인터페이스를 포함해야 합니다. - - - - File Server를 위한 다중 SATA Slot - - OS가 설치되어 작동할 SSD Slot - - 트래픽 분산을 위한 두 개 이상의 NIC - - - - - - 예산이 제한되어 있으므로 가능한 저렴한 제품을 선택합니다. - - - - 검토 사항 - - 서버 하드웨어 - - 직접 구성 - - 24/7 운영을 위한 저전력 프로세서는 주로 일체형 메인보드 형태로 판매합니다. 따라서, 직접 서버를 구성한다고 하더라도 구성이 PC를 조립하는 것에 비하여 자유롭지 않습니다. 또한, 메인보드 및 프로세스를 제외하더라도 케이스 및 여러 가지 인터페이스를 구매하기 위한 추가 비용이 있습니다. - - 기성 제품 - - 기성 제품은 특화된 기능을 저럼한 가격에 좋은 품질로 제공하는 경우가 많습니다. 특히 저전력 프로세서를 사용한 제품은 구성의 자유도 면에서 기성 제품이 오히려 더 좋은 가격에 더 좋은 품질로 구매할 수 있습니다. - - 결론 - - 기성 제품 중에서 프로젝트에 필요한 모든 인터페이스가 포함되어 있던, Aoostar 의  WTR Pro 모델로 서버 하드웨어를 결정하였습니다. - - 프로세서 - - WTR Pro 모델의 프로세서 선택 - - WTR Pro 모델은 Ryzen R7 5825U 모델과 Intel N150 모델 두 가지가 있습니다. WTR Pro의 프로세서를 선택하기 위하여 다음과 같은 점들을 고려하였습니다. - - 성능 조건 - - - - - - - - - - 프로세서 - - - - R7 5825U - - N150 - - - - - - - - - - Core/Thread - - 8/16 - - 4/4 - - - - - - Architecture - - x86-64 - - x86-64 - - - - - - Clock Speed - - 2.0GHz(4.5GHz as Turbo Speed) - - 0.8GHz(3.6GHz as Turbo Speed) - - - - - - L3 Cache - - 16MB - - 6MB - - - - - - TDP - - 15W(25W MAX) - - 6W - - - - - - Price(WTR Pro) - - 379,400 KRW - - 254,900 KRW - - - - - - - - 절대적인 성능은 R7 5825U 모델이 N150 모델보다 훨씬 좋습니다. Core는 2배, Thread는 4배이며 Clock Speed 역시 압도적인 격차가 있습니다. 하지만 프로젝트의 요구사항을 고려할 때, N150 모델 역시 충분히 이를 구현 가능한 성능을 가지고 있습니다. 하지만 N150 모델은 R7 5825U에 비하여 고부하 상황에서 언제나 쾌적한 환경을 제공하는 것이 힘들 수 있다는 것을 예상하고 실제 서비스 운영 시 자원 배분에 각별한 주의가 필요합니다. - - 24/7 운영을 위한 조건 - - 24/7 운영을 위하여 프로세서는 저전력, 저발열, 저소음, 안정성 조건을 만족하여야 합니다. R7 5825U 모델은 N150 모델에 비하여 매우 좋은 성능을 가지고 있습니다. 하지만 N150 모델에 비하여 전력 소모량 및 발열량이 높았습니다. TDP가 약 2.5~4배 가까이 차이가 났으며, 이는 가정에서 24/7로 운영하며 사용할 때 발열 및 전력 소모가 더 높은 것을 의미합니다. 이는 시스템에 안정성에도 큰 영향을 끼칠 것입니다. - - 가격 조건 - - R7 5825U 모델은 N150 모델에 비하여 약 30% 가량 비싸 약 120,000 KRW 정도의 추가 지출이 필요하였습니다. - - 기타 고려 사항 - - - - R7 5825U 모델은 다음과 같은 특정한 하드웨어 이슈를 가지고 있었습니다. - - - - 특정한 작업(트랜스코딩 등)이 N150을 사용할 때에 비하여 불안정함이 보고됨. - - SATA Controller가 Hypervisor 상에서 Path through 할 때 비정상적인 작동이 보고됨. - - 쿨링이 원활하지 못한 미니 PC 구조와 더불어 4Bay HDD를 지원하여, 발열이 쉽게 해소되지 않는 문제 보고 됨. - - - - - - R7 5825U 모델은 N150 모델에 비하여 RAM Slot과 M.2 Slot이 한 개씩 더 많습니다. - - - - 예상 프로세서 할당량 및 우선 순위(N150) - - - - - - - - - - 서비스 이름 - - - - - - 타입 - - - - - - 할당 vCPU (core) - - - - - - 우선 순위 (cpuunits) - - - - - - 할당 이유 - - - - - - - - - - - - OPNsense - - VM - - 2 - - 매우 높음 - - 트래픽 처리, IDS/IPS 학습 및 내부 방화벽, VPN 핵심 기능: 무중단 필수 - - - - - - - - Rocky Linux -FreeIPA  - - - - VM - - 2 - - 매우 높음 - - ID 관리(LDAP/SSO), 내부 CA, DNS 등 핵심 인증 서비스: 무중단 필수 - - - - - - - - AdGuard Home - - - - LXC - - 1 - - 낮음 - - 광고 차단을 목적으로 하는 DNS 필터링 서비스: 무중단 필수, FreeIPA 연동 - - - - - - - - Monitoring Server - - - - LXC - - 1 - - 낮음 - - 백그라운드 상 지속 운영하나, 부하 자체는 낮을 것으로 보임 - - - - - - Proxy Server - - LXC - - 1 - - 보통 - - SSL/TLS 처리시 순간 부하 발생 가능, 지속 운영 - - - - - - File Server - - VM - - 1 - - 보통 - - I/O 작업 및 파일전송 프로토콜, 접속자에 따라 변동 - - - - - - Web Server/WAS - - VM - - 1 - - 낮음 - - 저트래픽 기술 블로그, Bookstack 운영 (DB 서버 분리) 부하는 낮을 것으로 보임 - - - - - - DB Server - - VM - - 2 - - 높음 - - 중앙 집중식 데이터베이스 서버: 무중단 필수 - - - - - - Application Server - - VM - - 2 - - 높음 - - Immich, PeerTube, Vaultwarden 등 미디어 및 주요 서비스(DB 도커는 별도 운영) - - - - - - Kali Linux - - VM - - - - 2(1) - - - - 높음(낮음) - - - - 실행 시 Docker VM 일시 중지 조건부 운영 (+practice client) - - - - - - - - - - vCPU: VM/LXC에 할당되는 가상 CPU 코어 개별 VM/LXC에 pCPU의 Thread 초과 할당 비권장. 하지만 전체 VM/LXC는 초과 가능 cpuunits: CPU 자원 경합 시 값이 높은 쪽이 우선 순위가 높고 더 많은 시간을 할당 (매우 높음: 기본값 2배, 높음: 기본값 1.5배, 보통: 기본값, 낮음: 기본값 0.5배 등으로 설정) cpulimit: 보조적으로 과도한 CPU점유를 막기 위해 절대적인 CPU 사용 상한 제한 socket/core: vCPU의 CPU와 Core 개념, vCPU는 pCPU의 Thread 위에서 실행되며, 각각 soket(vCPU)과 core(vCore)로 구분 - - 결론 - - R7 5825U 모델은 N150 모델에 비하여 막강한 성능을 가지고 있습니다. 하지만 저부하 IPS/IDS 운용과 소규모 인원을 위한 WEB, Media, Application Service 운용, 매우 낮은 트랜스코딩 사용 가능성과 동시 부하 가능성이 매우 낮다는 프로젝트의 요구 사항을 반영하였을 때,  목적 달성에는 N150 모델로 충분할 것입니다. 이는 가장 중요한 부분으로 요구 사항을 넘어서는 고부하 상황 조건을 고려한다면 매우 고성능 서버 프로세서와 고용량 RAM으로도 성능의 부족을 경험할 수 있을 것입니다. 모든 하드웨어 선택은 항상 성능과 요구 사항 사이의 균형을 찾아야 합니다. 따라서 성능과 요구 사항 사이의 균형, 안정성 및 가격, 기타 고려 사항을 종합하여 최종적으로 N150 모델을 선택하였습니다. - - RAM - - RAM의 용량은 서비스의 가용성을 유지하기 위하여 매우 중요합니다. RAM 용량이 부족하면 일차적으로 Swap이 일어나 서비스가 느려집니다. 이후 지속적으로 RAM 용량이 부족하면 시스템은 자동으로 OOM Killer 기능을 이용하여 RAM을 차지하는 서비스를 강제로 중지합니다. 이는 서비스의 가용성의 큰 문제를 만듭니다. 따라서 RAM 용량을 항상 적절하게 할당하고 배분해야 합니다. - - 서비스 별 RAM 사용 예상량 - - - - - - - - 서비스 이름 - - 타입 - - 할당 RAM - - 할당 이유 - - - - - - - - - - OPNsense - - VM - - 4GB - - IDS/IPS 학습 및 내부 방화벽/VPN 핵심 기능 - - - - - - Rocky Linux -FreeIPA  - - VM - - 4GB - - ID 관리(LDAP/SSO), 내부 CA, DNS 등 핵심 인증 서비스 - - - - - - AdGuard Home - - LXC - - 0.5GB - - 광고 차단을 목적으로 하는 DNS 필터링 서비스, FreeIPA와 연동 - - - - - - - - Monitoring Server - - - - LXC - - 2GB - - Prometheus, Grafana, Loki, Promtail 기반 시스템 모니터링 및 로깅과 Ansible을 통한 서버 자동 설정 - - - - - - Proxy Server - - LXC - - 1.5GB - - 리버스 프록시 및 SSO 프록시, SSL 인증서 관리, DDNS 스크립트 작동 - - - - - - File Server - - VM - - 4GB - - NFS, WebDAV 등 데이터 저장 및 공유(SATA 패스스루 RAID 관리) - - - - - - Web Server/WAS  - - VM - - 2GB - - 저트래픽 기술 블로그, Bookstack 운영(DB 서버 분리) - - - - - - DB Server - - VM - - 4GB - - 중앙 집중식 데이터베이스 서버(여러 서비스에서 사용, 2~3개 DBMS 도커로 운용, 각 DBMS 별로 1~1.5GB 램 제한) - - - - - - Application Server - - VM - - 6GB - - Immich, PeerTube, Vaultwarden 등 미디어 및 주요 서비스(DB 서버 분리) - - - - - - Kali Linux  - - VM - - (4GB + 1GB) - - - - 실행 시 Application Server VM 일시 중지 조건부 운영 (+practice client) - - - - - - - - Guest RAM - - - - - 28GB(27GB) - - 일반 환경 시 RAM(Kali 실습 시 RAM) - - - - - - PVE - - OS - - 4GB(5GB) - - Btrfs 스냅샷 기능 및 OS 안정성 확보(kali 실습 시 5GB) - - - - - - - - 결론 - - 계획 초기 현 계획보다 적은 기능을 고려하여 16GB RAM으로 충분히 모든 서비스를 제공할 수 있을 것이라 생각했습니다. 하지만 위와 같이 계획이 구체화 되면서 각 서비스를 제공하기 위한 필요 RAM 용량이 늘어났습니다. 따라서 최종적으로 32GB RAM 을 선택하였습니다. 각 서비스가 동시에 고부하 작업을 할 확률이 매우 낮은 점, 각 서비스 별로 RAM 용량의 여유를 둔 점, PVE 자체의 KSM(Kernel Same-page Merging) 기능을 통한 메모리 절약이 가능한 점을 고려하면 32GB의 RAM 용량은 요구 사항을 충분히 충족할 수 있을 것입니다. 하지만 실제로 서비스를 운영하며 RAM 할당량을 모니터링 및 조율할 필요가 있습니다. - - 저장 장치 - - SSD - - OS가 설치되는 부분 및 DB가 실행되는 부분은 IO 속도가 매우 중요하므로 M.2 NVMe SSD를 선택하여 사용하기로 선택하였습니다. OS 및 DB 데이터를 비롯해 각종 로그 데이터를 저장하기 위해 충분한 용량인 M.2 NVMe SSD 1TB  모델을 선택하였습니다. - - 서비스 별 SSD 사용 예상량 - - - - - - - - 서비스 이름 - - 타입 - - 할당 SSD - - 할당 이유 - - - - - - - - - - OPNsense - - VM - - 32GB - - 단기 로그, IPS/IDS 규칙, 캐시 등을 저장 - - - - - - Rocky Linux -FreeIPA  - - VM - - 64GB - - LDAP 데이터베이스, 인증서, 사용자 정보 등을 저장 - - - - - - AdGuard Home - - LXC - - 16GB - - 단기 쿼리 로그 등 저장 - - - - - - - - Monitoring Server - - - - LXC - - 64GB - - 단기 메트릭, 로그 저장. 추후 File Server로 옮기더라도 충분한 용량 부여. - - - - - - Proxy Server - - LXC - - 16GB - - 웹 캐시 등을 저장 - - - - - - File Server - - VM - - - - 32GB - - - - OS 및 Docker 설치 - - - - - - Web Server/WAS  - - VM - - 32GB - - OS 및 Docker 설치(Git 혹은 Application Data는 File Server NFS 이용) - - - - - - DB Server - - VM - - 128GB - - DB는 I/O 성능이 중요, DB 데이터 저장(백업 파일; Dump는 File Server NFS 이용 저장) - - - - - - Application Server - - VM - - 128GB - - DB 데이터는 DB Server, 대용량 파일은 File Server 이용하나 Cache 및 Metadata, Tumbnail 등 저장 - - - - - - Kali Linux + Practice Client - - VM - - 64GB + 16GB - - Kali와 Clients OS 데이터 저장 - - - - - - PVE - - OS - - 100GB - - PVE 자체 용량(자동 지정) - - - - - - 총합 - - - - 692GB/1TB - - SSD 성능 상 필요 예비 공간 100GB, 추후 필요시 약 150GB~200GB 추가 할당 가능 - - - - - - - - HDD - - 이에 반하여 일반 데이터 파일 및 미디어 파일이 저장되는 부분은 IO 속도보다는 용량과 안정성이 중요합니다. 따라서 저장 용량 대비 가격이 저렴한  2TB 3.5" HDD 4개 를 사용하기로 선택하였습니다. - - 이와 동시에 저장장치의 가용성을 확보하기 위하여 여러 RAID(Redundant Array of Independent Disk) 방식을 고려하였습니다. 이 때, RAID 10, 5, 6이 고려하였고, HDD 갯수가 4개인 점, IO 성능의 향상폭이 가장 높은 점, 데이터 복구시 HDD 별 부하량이 가장 낮은 점과 같은 고려사항을 통해 RAID 10 (총 8TB 중 4TB 사용 가능)을 선택하였습니다. - - 결론 - - 다음과 같은 조건 및 고려사항 하에서 WTR Pro N150 모델은 주요 기능을 운영하고 학습 목표를 실행하는데 있어서 큰 무리가 없다고 판단됩니다. 하지만 N150 프로세서의 절대적인 성능의 한계에 대하여 명확하게 파악하고, 실제로 고부하 작업(특히 미디어 스캔 혹은 트랜스코딩 등)의 상황에서 쾌적하지 못한 상황이 발생할 수 있다는 것을 인지하여야 합니다. 하지만 실제 요구 조건 하에서 그런 상황은 거의 일어나지 않을 것이므로 실제 운영 상 성능 여유(Headroom)가 없다는 점만 인지하고 있으면 충분할 것으로 보입니다. 따라서 현재 가지고 있는 N150 서버에 대한 꾸준한 모니터링 및 성능 할당량 조정을 통하여 제한된 하드웨어 조건 하에서 어떻게 가용성을 지킬 수 있는 가에 대한 부분을 학습 할 수 있을 것입니다. - - 최종적인 하드웨어 선택 및 각 구성은  1.2. 하드웨어 구성 에 정리되어 있습니다. - - - - 2025-05-24 - 초안 작성 - - 2025-05-25 - HDD RAID 부분 수정 - - 2025-05-28 - RAM 용량 조정: SSO proxy와 reverse proxy 분리로 인한 RAM 0.5GB 추가/모니터링 서버 RAM 0.5GB 추가/ Proxmox RAM 1GB 감소/ kali 실습시 Practice client 추가로 +1GB/DB 서버 분리, 도커 환경을 위한 VM 변경 및 램 2GB 추가/OPNsense 2GB 감소 - - 2025-05-29 - 예상 프로세서 할당량 및 우선 순위 추가 / SSD 용량 1TB로 상향(로그 및 DB 데이터) - - 2025-06-16 - AdGuard Home LXC 내용 추가 및 RAM 용량 조정/ Proxy Server 0.5GB 추가/ AdGuardHome LXC 0.5GB 추가/PVE 1GB 감소 - - 2025-06-17 - 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-19 - Rocky Linux 내용 추가 - - 2025-06-20 - 날짜 표기 변경 - - 2025-06-24 - 각 VM/LXC 별 SSD 용량표 추가 - -2.2. 네트워크 설계 -목표 - - - - 네트워크 설계 중 물리적인 한계 및 하드웨어적인 한계를 파악하고 이를 극복할 수 있는 기술적인 방안을 선택합니다. - - 최종적으로 설계된 네트워크 구조의 잠재적인 위험성을 파악하고 장애 발생시 어떻게 대처 할지 구상합니다. - - 각 과정의 고려사항 및 근거를 명확하게 기록합니다. - - - - 요구사항 - - - - 집안 네트워크 회선의 물리적인 구조로 인한 제약 사항을 파악 및 회피합니다. - - 주요 네트워크 장비의 VLAN 미지원에 대응하여 L2 통신간 E2E 보안 및 클라이언트 격리 방안을 모색합니다. - - 방화벽 및 IPS/IDS로 Inbound, Outbound 통신을 중앙에서 감시 및 제어할 수 있도록 합니다. - - L2 통신과 L3 통신의 차이를 명확히 파악하여 각 통신별 보안 대책을 수립합니다. - - - - 검토사항 - - 물리적 네트워크 구조 구성도 - - - - 물리적 제약 사항 - - - - Terminal Box 내의 전원의 부재로 인하여 기반 네트워크 장비 PoE 사용(새로운 Node를 추가 불가) - - 각 방과 Terminal Box 사이의 물리적인 회선이 하나 - - 주요 네트워크 장비(T5004, AX6000M, H5008G)의 VLAN 미지원으로 인하여 PVE 밖 Clients에 VLAN 적용 불가 - - T5004의 DHCP Relay 미지원으로 OPNsense의 DHCP 운영 불가 및 서브네팅 활용 불가 - - - - 이중 게이트웨이 구조 - - 제약 사항 - - OPNsense가 Gateway가 되어 DHCP를 운영하기 위해서는 Terminal Box에 위치하거나, DHCP Relay 기능을 활용하여야 합니다. 하지만 Terminal Box에 전원이 부재한 제약 사항으로 인하여 불가능합니다. 또한 T5004는 VLAN을 미지원하고, DHCP Relay 기능 역시 미지원하여 불가능합니다. 이는 OPNsense가 직접 DHCP를 운영하고 Gateway로 작동하는 것이 불가능함을 뜻합니다. - - 회피 방안 및 결론 - - T5004 내에서 DHCP 운영시 DHCP의 기본 Gateway를 변경할 수 있는 점에서 착안하여, OPNsense를 T5004의Untagged VLAN의 DHCP 기본 Gateway로 설정합니다. 이때 OPNsense는 Static IP를 할당하여 자신의 Gateway를 T5004로 설정하는 이중 게이트웨이 구조를 운영할 수 있습니다. 이 경우 다음과 같은 예상되는 위험이 존재합니다. - - OPNsense의 IP가 Static IP가 아닌 경우, 자기 자신을 게이트웨이로 지정하여 Routing loop 현상이 발생 가능 - - 하지만 OPNsense의 IP가 Static IP로 설정되어 T5004로부터 IP를 할당받는 경우가 아니라면 Routing loop의 위험성은 없습니다. 동시에 모든 L3 통신은 OPNsense를 거치게 됩니다. 따라서, 물리적으로 T5004 바로 뒤에 방화벽 OPNsense를 두지 못하는 상황에서 이러한 이중 게이트웨이 구조는 가장 현실적으로 L3 통신을 통제할 수 있는 방안입니다. - - 또한 PVE 내부의 있는 서버 및 서비스들은 vmbr을 통한 VLAN 적용이 가능하므로 OPNsense의 라우팅 기능을 활용하여 라우팅 테이블을 작성하여야 합니다. - - - - Untagged VLAN - Untagged VLAN 통신: ARP를 통한 unicast 통신 - - Untagged VLAN - Other VLAN 통신: OPNsense가 내부망의 게이트웨이이므로, OPNsense 자체 라우팅 - - Untagged VLAN - WAN 통신: OPNsense가 내부망의 게이트웨이이므로, OPNsense에서 T5004로 라우팅 - - Other VLAN - WAN 통신: OPNsense가 내부망의 게이트웨이이므로, OPNsense에서 T5004로 라우팅 - - - - OPNsense의 NIC 구성 방안 - - 제약 사항 - - OPNsense에 할당 가능한 물리 NIC는 2개입니다. 그리고 T5004는 LACP(Link Aggregation Control Protocol)를 지원하지 않습니다. 이러한 상황 속에서 설계 목표 중 하나였던 WAN 통신은 NIC0에서, LAN(VLAN 포함) 통신은 NIC1에서 제어하여 병목을 최소화하고, 모든 L3 통신을 OPNsense 중앙에서 통제하여야 합니다. - - 회피 방안 및 결론 - - - - 위 제약 사항을 해결하기 위하여 다음 다이어그램과 같이 두 가지 방안을 고려하였습니다.  - - - - NIC0과 NIC1를 vmbr0에 통합하여 OPNsense 내 vtnet0과 연결하여 단일 인터페이스로 관리 - - NIC0과 NIC1 각각 vtnet0, vtnet1로 할당하여 따로 관리 - - - - 방안 1은 OPNsense에서 Untagged VLAN에 대하여 단 하나의 인터페이스와 IP만을 관리하므로 설정이 단순하고, LACP를 지원하지 않더라도 Active-backup 설정시 자동으로 NIC 장애 대처가 가능한 점이 있습니다. 하지만 프로젝트의 목표였던 WAN과 LAN 통신의 NIC 분리가 불가능합니다. 이는 OPNsense의 단일 vtnet으로 트래픽이 전달되며 vmbr0은 가상 L2 장비로 NIC를 구별하지 않기 때문입니다. 또 Active-backup 설정시 평상시에는 하나의 주 물리 NIC만 사용되므로 NIC 별 효과적인 트래픽 분산을 하는 것이 불가능합니다. - - 2번 방안은 두 물리 NIC에 각각 vmbr에 연결하여, OPNsense에 할당하고 각각 별도의 인터페이스 및 IP로 관리합니다. 따라서 OPNsense의 L3 인터페이스를 통해 물리적인 NIC 경로를 WAN 트래픽과 LAN 트래픽을 분리하여 사용할 수 있습니다. 이를 통해, vtnet0과 vtnet1에 각각에 방화벽 규칙 등을 적용하여 정교한 통제가 가능해지며, 두 물리 NIC를 전부 적극적으로 활용할 수 있습니다. 하지만 설정이 1번 방안에 비하여 더 복잡해집니다. 각 물리 NIC가 장애 시, 각 NIC가 가진 역할이 중단될 수 있는 SPOF가 됩니다. 또한 명시적으로 라우팅 정책을 설정하지 않을 시, 비대칭 라우팅으로 인한 네트워크 장애가 발생할 수 있습니다. - - 최종적으로는 다음과 같은 이유로 방안 2를 선택하였습니다. - - - - L2/L3 통신의 차이를 명확하게 하고, OPNsense의 라우팅, 방화벽 정책, 인터페이스 관리에 대한 더 깊은 이해 가능 - - WAN 통신은 NIC0에서, LAN(VLAN 포함) 통신은 NIC1에서 처리한다는 초기 설계 목표를 명시적으로 L3상에서 구현 가능 - - 두 개의 별도 물리 NIC를 적극적으로 활용하여, 성능의 이점을 가짐 - - 이중 게이트웨이 구조의 명확화 - - - - Zero Trust 원칙 하 클라이언트 방화벽 정책 제어 - - 제약 사항 - - T5004를 비롯한 홈 네트워크 장비는 VLAN 및 Subnet을 지원하지 않습니다. 이는 PVE 밖의 클라이언트에 대한 유연한 논리적 L2 망 분리가 불가능함을 뜻합니다. LAN 통신은 L2 통신으로, L3 장비인 방화벽(OPNsense)을 거치지 않고 Gateway 또한 통과하지 않습니다. L2 통신에서는 클라이언트가 직접 ARP broadcast를 통해 전송 대상의 MAC 주소를 획득하여 unicast로 통신합니다. 따라서 VLAN과 Subnetting이 불가능하므로 방화벽을 통한 ACL(Access Control List) 관리는 불가능합니다. 이는 내부망을 위한 보안 대책의 필요성을 높입니다. 다만, PVE 내부의 클라이언트에 대하여서는 VLAN을 통한 L2 망 분리가 가능합니다. - - 회피 방안 및 결론 - - 외부자가 쉽게 네트워크를 접근할 수 없도록 WPA3 프로토콜을 이용한 통신 암호화가 필요합니다. 또한 VLAN 기능은 없지만, T5004에 존재하는 Guest network 기능을 활용하여 외부인이 사용 가능한 네트워크를 격리할 수 있습니다. Guest network는 LAN 통신을 막고, WAN 통신만 가능하도록 하는 기능입니다.  각 VLAN(Untagged VLAN 포함) 내부 통신의 ACL 정책은 클라이언트 단에서 처리되어야 합니다. 이 때, Zero Trust 원칙 하 각 서버 및 클라이언트의 iptables, Windows 방화벽 등 자체 방화벽 정책을 철저하게 설정하여 최소 권한 원칙을 통해 꼭 필요한 포트와 IP로 부터의 접근만을 허용해야 합니다. 이 때 홈 네트워크 특성상 관리해야 할 서버 및 클라이언트의 숫자가 크지 않으므로 이러한 조치들로도 과도하게 복잡해지지 않고 충분히 관리가 될 것으로 보입니다. 또한 Ansible 등의 서버 설정 자동화 툴 도입 역시 관리 피로를 낮출 수 있습니다. - - 각 서버 및 클라이언트의 대역대를 VLAN으로 나누어 각 통신을 L3 장비인 OPNsense에서 담당할 수 있도록 한다면 더욱 확실하게 이러한 문제를 해결할 수 있을 것으로 보입니다. 특히 WTR Pro 내부 VLAN2, 3, 4에 접근 가능한 IP를 OPNsense에서 직접 제어할 수 있기 때문에 다음과 같은 정책을 시도할 것입니다. - - - - 내부 접근 가능 IP 제한(특정 Client IP) - - 외부 접근 시 VPN 이용 접근만 가능 - - 특정 서비스(Reverse Proxy 등)만, 서비스 목적에 따라 필요한 범위 내에서 전체 접근 허용 - - - - 확인 결과 T5004에 OpenWRT 적용이 가능한 것을 확인하였습니다. 현재 계획 대로 On-premise 환경을 구축한 이후, T5004에 대한 OpenWRT 적용 및 전체 네트워크 VLAN 적용을 테스트 해볼 수 있을 것으로 보입니다. 다만 VLAN을 통한 망 분리를 넘어, DMZ (Demilitarized Zone) 를 구성하는 경우 홈 네트워크 특성 상 24/7 관제가 불가능하며 CERT(Computer Emergency Response Team) 운용 역시 불가능합니다. 따라서 해당 DMZ를 운영시 오히려 관리 지점 및 보안 취약점을 늘리는 결과를 만들 수 있습니다. 따라서, OpenWRT를 적용한 프로젝트를 진행하더라도 추가로 DMZ를 운영하지는 않을 것입니다. - - 통신 보안 및 암호화 - - 제약 사항 - - WAN 통신 시에는 외부 CA를 통한 TLS 인증서를 발급 받아 통신을 비교적 쉽게 암호화 할 수 있습니다. 하지만 내부망 통신시에는 외부 TLS 인증서를 통한 암화가 불가능합니다.  또한 내부망을 위한 일부 특정 취약 프로토콜들(NFS, SMB 등)은 TLS를 통한 암호화가 불가능하며 평문 통신을 하므로 이를 암호화 할 방안이 필요합니다. LAN을 위한 특정 프로토콜들은 WAN에서 접근 시 사용이 불가능하기에 이를 위하여 VPN 역시 도입이 필요합니다. - - 회피 방안 및 결론 - - 기본적으로 WAN에서 접근 시 웹 서비스를 위한 https(443)와 VPN을 위한 포트만을 T5004에서 NAT로 개방합니다. 이를 통해 WAN에서 LAN을 위협하는 포트 스캔 등은 막을 수 있습니다. 이는 OPNsense의 부하 역시 줄여줍니다.  또한 WAN에서 IP를 통한 직접 접근을 막고, Domain을 통한 접근만 허용하며, Cloudflare를 통한 DDoS 방어 서비스를 사용한다고 모든 웹 서비스는 Reverse Proxy를 거치게 한다면 더욱 안전한 WAN 통신이 가능할 것으로 보입니다. T5004 내에서도 역시 외부 관리페이지 접속 차단, UPnP등 보안 취약점 기능 전체 차단, 강력한 PW 사용, 최신 펌웨어 유지 등의 조치가 필요할 것으로 보입니다. - - WAN 통신 뿐 아니라 LAN 통신 시 암호화를 적용하기 위하여 FreeIPA를 통한 내부 CA(TLS 암호화) 구축 및 Kerberos(NFS, SMB 등 암호화 및 추후 SSO 구현 토대) 인증 구조를 구현하고자 합니다. 또한 장기적으로 LDAP 및 SSO를 구현하는 데 있어 Kerberos가 사용될 수 있으므로 통합하여 FreeIPA를 구축합니다. FreeIPA 구축 시, 내부 root CA를 통한 내부 도메인용 인증서 직접 서명/발급/갱신/폐기 등을 자동화 할 수 있는 스크립트를 작성하는 것도 중요합니다. - - FreeIPA는 자체 DNS 기능 역시 포함하고 있으므로 Local DNS를 구축하여 외부와 내부에서 같은 Domain을 통한 접근이 가능하도록 Split Horizon DNS를 구현하고자 합니다. 이때 Adguard Home을 통한 광고 및 악성 Domain을 차단하고 FreeIPA와 연동할 수 있습니다. 또한 Local DNS에 없는 Domain을 요청 받은 경우 신뢰할 수 있는 외부 DNS로 DNS 포워딩을 하여 구현 가능합니다. DoH를 통하여 Cloudflare Public DNS를 이용합니다. - - WAN을 통한 내부 서버 및 클라이언트 관리는 지양하고, 필요시 VPN을 통해 관리합니다. 또한 LAN 서비스(AdGuard Home 등)을 이용시 역시 VPN을 통한 LAN 접속 후 이용합니다. - - SFOP(단일 실패 지점) 문제 - - 제약 사항 - - OPNsense와 FreeIPA는 홈 네트워크에서 가장 중요한 부분을 차지하고 있는 서비스입니다. 이 서비스들에 이상이 생긴다면, 다른 하드웨어 장비가 이상이 없더라도 전체 네트워크가 중단될 수 있습니다.  - - - - OPNsense: 게이트웨이 + 방화벽 + VPN 서버 - - FreeIPA: 내부 CA + 내부 DNS + KDC - - - - 물론 L2 통신은 ARP를 통한 MAC 기반 unicast 통신으로, 이들에 문제가 있더라도 가능하지만 Outbound/Inbound 통신이 불가능해 지는 시점에서 홈 네트워크의 가용성에 치명적인 타격을 줄 수 있습니다. - - 회피 방안 및 결론 - - 가장 좋은 해결 방안은 Scale out을 통한 물리적 HA 구성이겠으나 이는 예산의 문제로 현재 불가합니다. 또한 프로젝트 목표 역시 상용 서비스 수준을 따라가는 것이 아닌 학습과 체험 목적 역시 존재하므로 SPOF 문제는 어느 정도 타협이 필요합니다. 따라서 이러한 문제를 최대한 회피하기 위하여 다음과 같은 방안을 모색했습니다. - - - - OPNsense 및 FreeIPA의 설정 백업 (각 서비스 별 기능 및 Gitea, Ansible 활용) - - Proxmox 내 기능 활용하여 VM 스냅샷 백업 - - 모니터링 서버를 이용하여 이상 발생시 알림 발송(단 Outbound/Inbound 통신 불가시 메일 발송 등은 불가함) - - Proxmox 내에서 주기적으로 각 VM의 상태를 체크하여 문제 발생 시 자동으로 재시동, 문제 지속 시 스냅샷으로 VM 복원을 진행 - - - - 다음과 같은 방안들이 자동화 된 스크립트에서 작동한다면 RTO(목표 복구 시간)을 최소화 하고 RPO(목표 복구 지점)의 관리가 가능할 것으로 예상됩니다. - - VM/LXC/Docker/Client 간 통신 - - PVE는 기본적으로 Linux Bridge(vmbr) 및 vNIC(vtnet)을 통해 각 VM/LXC에 할당이 가능하며, Docker 역시 dockerd를 통해 내부 Bridge를 이용하여 통신하는 것이 가능합니다. - - 이를 통하여 서버 망과, 특수 목적 망(Kali 및 실습 목적 LAN)은 물리적 NIC에 연결되지 않은 vtnet과 vmbr을 할당하여 OPNsense 내에서 VLAN을 통해 격리하고자 합니다. 모든 VLAN은 OPNsense를 게이트웨이로 갖습니다. - - Docker의 통신은 특수한 경우에만 macvlan 기능을 사용하여 직접 IP 주소를 부여합니다. 이외에는 host 내부에서 Bridge 네트워크를 통해 통신하며, 외부 통신 필요시 host의 IP 주소와 port를 기반으로 통신합니다. - - 결론 - - 다음과 같은 검토 사항을 통하여 볼 때 현실적인 물리적/하드웨어적 제약 하에서도 원하는 기능 및 구조를 구현하는 것에는 무리가 없다고 여겨집니다. 하지만 제약 조건을 회피할 수 있는 것이지, 한계점이 사라지는 것은 아니기에 철저한 정책 수립과 꾸준한 모니터링 및 로깅, 그리고 조정이 필요한 것은 인지하여야 합니다. 이 과정을 통하여 언제 어떠한 프로토콜이 어떠한 방식으로 어떻게 작동하는지에 대하여와 전반적인 네트워크 통신의 구조에 대하여 깊은 이해를 하고자 합니다. - - 최종적인 네트워크 설계 구조는 1.3. 최종 목표 아키텍처 에 정리되어 있습니다. - - - - 2025-05-25 - 초안 작성 시작 - - 2025-05-28 - 초안 작성 완료 - - 2025-05-30 - VLAN 관련하여, Hypervisor 내부 서버에 대한 VLAN 분리 적용 - - 2025-06-02 - OPNsense의 NIC 구성 방안에 대한 내용 추가 - - 2025-06-17 - 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-20 - 날짜 표기 변경 - - 2025-06-23 - VLAN 표기 방식 수정 - -2.3. 시스템 선정 -목표 - - - - 하나의 하드웨어(WTR Pro) 안에서 가상화 기능을 통해 각 사항을 중요도와 특성에 따라 논리적으로 분리 구축합니다 - - 요구 사항을 SoC 원칙 하 나누어 이를 효과적으로 달성할 수 있는 시스템을 식별 및 선정합니다. - - 선정된 OS와 서비스를 논리적으로 분리할 가상화 기술의 특징 및 장단점을 명확하게 하여 적재적소의 기술을 선택합니다. - - 각 과정의 고려사항 및 근거를 명확하게 기록합니다. - - - - 요구사항 - - - - 하나의 하드웨어 안에서 여러 시스템이 격리되어 실행될 수 있는 Hypervisor를 사용합니다. - - 각 시스템 다음과 같은 프로젝트 요구 기능을 구현할 수 있어야 합니다. - - - - 각종 서비스 제공을 위한 DB, local DNS, CA, Proxy, Firewall 등 서버 및 네트워크 운영 - - 파일, 미디어, 사진, 어플리케이션 서버 등의 개인 클라우드 환경 구축 - - 블로그, wiki 등의 웹 서비스 호스팅 - - IPS/IDS, Kali 등을 통한 정보보안 실습 환경 제공 - - - - - - 위와 같은 요구 기능들이 SoC 원칙 하에서 어떻게 분리되어 구축할지 정의해야 합니다. - - VM/LXC/Docker의 장단점 및 논리적 분리 정도 차이를 명확히 파악하여, 각 기술별 보안 대책을 수립합니다. - - - - 검토 사항 - - 시스템 선정에 가장 중요한 핵심 고려 사항 - - - - Open Source로 무료 사용이 가능할 것 - - 무료 버전의 기능상 제약이 없거나 적을 것 - - 실행 사양이 높지 않을 것 - - 사용자가 많아 정보를 찾기 쉬울 것 - - - - Hypervisor - - 프로젝트가 성공적으로 구현되기 위해서 가장 중요한 것은 하나의 하드웨어에서 여러 가지의 시스템이 동시에 논리적으로 격리되어 작동할 수 있어야 한다는 것입니다. 이를 위한 개념이 바로 Hypervisor입니다. Hypervisor는 하나의 하드웨어 자원을 가상으로 논리적 분할하여, 각 시스템들이 마치 별도의 독립된 하드웨어에서 시스템이 동작하는 것 처럼 작동할 수 있도록 지원합니다. 이 중 직접 HW 위에서 동작하는 Hypervisor를 Type 1이라고 부릅니다. - - Type 1 Hypervisor OS는 MS의 Hyper-V, VMWare의 ESXi, Xen, Proxmox VE 등 여러 종류가 있습니다. 이 중 프로젝트에 가장 부합하는 Hypervisor를 찾기 위해 각 시스템을 비교하고, 다음과 같은 사항을 고려했습니다. - - 시스템 비교 - - - - - - - - 시스템 - - Hyper-V - - ESXi - - Xen - - Proxmox - - - - - - - - - - 아키텍처 - - Windows Server - - 베어 메탈 - - 베어 메탈(Dom0 필요) - - KVM(Debian Linux base) - - - - - - 라이센스 - - - - Windows server에 포함 - - (무료 버전 지원 중단) - - - - - - 무료/유료 - - (기능 차이 존재) - - - - Open Source (상용 버전 존재) - - - - Open Source - - (모든 기능 무료) - - - - - - - - UI - - - - GUI, CLI, Web UI - - 모두 지원 - - - - - - CLI, Web UI 지원 - - - - - - CLI 기본 지원 - - GUI는 서드파티 지원 - - - - CLI, Web UI 지원 - - - - - - - - 핵심 기능 - - - - - - Windows/Linux guest 실시간 마이그레이션, HA 지원 등 - - - - - - Windows/Linux/Mac 등 다양한 Guest 실시간 마이그레이션, HA 지원 등 - - - - - - Windows/Linux/BSD 등 다양한 Guest, 실시간 마이그레이션, HA 지원 등 - - - - - - Windows/Linux/BSD/LXC 등 다양한 Guest 실시간 마이그레이션, HA, 통합 백업 및 복원 등 - - - - - - - - - - 최소 사양 - - - - - - 높음 - - - - - - 중간 - - - - - - 중간 - - - - - - 낮음 - - - - - - - - - - 추가 고려 사항 - - - - Linux 기반일 것 (다른 서비스와 호환성 고려) - - - - 결론 - - 각 시스템에 대한 비교와 고려 사항을 검토한 결과 가장 적합한 Hypervisor OS로 Proxmox VE 를 최종적으로 선택했습니다. - - 내부 라우팅 및 Firewall - - 철저한 SoC 원칙에 따른다면 라우팅과 Firewall 역시 분리되어야 하지만, 제한된 조건 하에서 WAN-LAN 통신 간 모든 패킷이 통과하는 내부 게이트웨이에 Firewall이 같이 있는 것이 큰 문제는 아닐 것이라 판단했습니다. 실제로 분리하여 구축한다고 하더라도, Friewall은 주로 게이트웨이 바로 앞에 물리적으로 위치하게 되며, 이들의 분리는 SoC에 따른 것이라기 보다는 SPOF를 막기 위한 것입니다. - - 내부 라우팅과 Firewall, IPS/IDS 기능을 모두 지원하는 OS로는 크게 pfSense와 OPNsense 두 가지입니다. pfSense가 2006년 출시되었고, OPNsense는 2014는 pfSense로 부터 포크되어 출시되었습니다. 같은 뿌리를 가지고 있는 만큼 두 OS는 비슷한 점도 많지만, 다른 점도 생겨났습니다. 이 중 프로젝트에 가장 부합하는 OS를 찾기 위하여 다음과 같은 사항을 고려했습니다. - - 시스템 비교 - - - - - - - - 시스템 - - pfSense - - OPNsense - - - - - - - - - - 출시 - - 2006 - - 2014 - - - - - - 아키텍처 - - FreeBSD - - FreeBSD - - - - - - 라이센스 - - CE/Plus 이원화 (기능 차이 존재) - - 완전한 Open Soruce (기술지원 버전 존재하나 기능은 동일) - - - - - - UI - - 전통적이고 안정적, 간결하며 페이지 리로드 적음 - - 현대적이고 사용자 친화적, AJAX를 활용한 실시간 업데이트 - - - - - - 핵심 기능 - - - - Firewall, IPS/IDS, NAT, VPN(OpenVPN/IPsec), 강력한 플러그인 지원 (pfBlockerNG, Suricata 등) - - - - - - Firewall, IPS/IDS, NAT, VPN(OpenVPN/WireGuard), 2FA - - - - - - - - 업데이트 주기 - - - - CE는 주기가 길며 안정성 중심, Plus는 더욱 잦은 업데이트 - - - - 연 2회 주요 릴리즈를 포함하여 잦은 업데이트 - - - - - - 커뮤니티 - - 방대하고 활발한 커뮤니티 - - 빠른 성장 중인 커뮤니티와 개발팀과 원할한 소통 - - - - - - 사양 - - 낮음 - - 낮음 - - - - - - - - 추가 고려 사항 - - - - 사용자 친화적인 UI를 가지고 있을 것 - - - - 결론 - - 본 프로젝트는 Firewall 및 IPS/IDS의 모든 고급 기능(추가적인 고급 플러그인 사용 등)을 사용하는 것을 목표로 하는 것이 아닙니다. 이러한 Firewall 및 IPS/IDS의 기능을 체험하고, 경험하며 익숙해지는 것이 목표입니다. 이런 상황 속에서 두 OS의 차이가 크게 없다면 Open Source이고 기본 기능의 제한이 없는 쪽을 선택하는 것이 맞다고 판단했습니다. 또한 아직 익숙하지 않은 상황에서 사용자 친화적인 UI는 기술 학습 곡선을 조금 더 완만하게 만들 수 있을 것으로 기대합니다. 따라서 기본 기능의 제한이 없고 조금 더 사용자 친화적인 UI를 갖춘 OPNsense 를 선택했습니다. - - LDAP, 인증, 감사 서비스 및 DNS - - 프로젝트를 구체화 하는 과정 속에서 통합 ACL 관리 및 SSO 인증, 내부 CA를 통한 통신 보안을 구현하고 추가적으로 내부 DNS를 운영하기 위한 통합 관리 솔루션이 필요합니다. 물론 각각의 기능을 별도로 분리하여 구축하는 것도 좋지만, 이는 시스템의 관리 포인트를 무척 늘리고 유지 보수를 어렵게 합니다. 앞서 언급했듯 본 프로젝트의 주된 목표는 각각의 기능을 체험하고, 경험하고 익숙해 지는 것입니다. 각각의 기능을 별도로 구축하는 것 보다는 이러한 통합 인증 서버를 구현할 수 있는 시스템을 선택하는 것이 유리하다고 판단했습니다. 이러한 시스템은 크게 FreeIPA, Samba AD DC, Microsoft Active Directory 등이 있었습니다. - - 시스템 비교 - - - - - - - - - - 시스템 - - - - FreeIPA - - Samba AD DC - - Microsoft Active Directory - - - - 일반 서버 OS 하 - - 개별 솔루션 조합 - - - - - - - - - - - - 아키텍처 - - Linux/Unix 중심 - - Active Directory 호환 - - Windows 중심 - - 필요 기능만 조합 - - - - - - 라이센스 - - - - Open Source - - - - Open Source - - 유료 - - 서비스 별로 다름 - - - - - - UI - - - - CLI(ipa 명령어), Web UI 지원 - - - - - - CLI(samba-tool), Windows RSAT 지원 - - - - - - CLI, GUI 지원 - - - - 통합 UI 부재 - - - - - - - - 핵심 기능 - - - - - - LDAP, Kerberos, DNS, CA, 정책 관리, OTP 등 지원 - - (Linux/Unix 환경 중심) - - - - - - LDAP, Kerberos, DNS, 정책 관리, AD 스키마 호환 등 지원 (Winodws/linux/unix - - 호환 중심) - - - - - - LDAP, Kerberos, DNS, CA, 정책 관리, ADFS 등 지원 (Windows 중심) - - - - - -   - - 필요한 프로토콜 및 - - 기능 별도로 구현 - - - - - - - - - - 설정 및 - - 유지보수 난이도 - - - - - - 높음 - - - - - - 높음 - - - - - - 중간 - - - - - - 매우 높음 - - - - - - - - - - 최소 사양 - - - - 중간 - - 중간 - - 높음 - - 가변적 - - - - - - - - 추가 고려 사항 - - - - 현재 목표 구조는 주로 Linux와 Unix를 사용하므로 이에 대한 호환성이 좋을 것 - - - - 결론 - - 현재 목표로 하는 홈 네트워크 환경은 주로 Linux와 Unix를 기반으로 설계되어 있습니다. Samba AD DC나 Microsoft Active Directory 같은 경우 Linux와 Unix 자체에 초점이 맞춰진 것이 아닌, Linux/Unix - Windows 간 호환성 혹은 Windows 자체에 초점이 맞춰져 있었습니다. - - 거기에 개별 솔루션 조합은 아무리 본 프로젝트가 학습과 경험에 조금 더 초점을 맞춘다고 할지라도, 중앙 관리 및 모니터링이 불가능한 점은 너무나 많은 관리 포인트를 만들어 프로젝트 진행을 어렵게 만들 것이라 판단했습니다. - - 따라서 본 프로젝트에 가장 잘 어울리는 시스템으로 Rocky Linux 위의 FreeIPA 를 선택했습니다. 최대한 비영리 Open Source를 사용하고자 OS로 Rocky Linux를 선택하였습니다. 단, FreeIPA를 통한 LDAP를 구현하더라도 관리의 편의를 위하여 별도의 UID/GID 대역 매트릭스를 운영합니다. - - 또한, DNS를 통한 광고 필터링이 가능한데 이를 FreeIPA에 직접 구현하는 것은 관리상 큰 어려움이 있습니다. 따라서 이를 쉽게 구현하기 위하여  AdGuard Home 서비스를 LXC로 별도 구축하여 FreeIPA의 DNS와 연결합니다. - - 중앙 모니터링 및 로깅, 서버 설정 자동화 - - 프로젝트의 핵심 하드웨어인 WTR Pro의 경우 N150 프로세서와 32GB RAM을 사용하고 있어 성능의 여유(headroom)이 매우 한정적인 것이 사실입니다. 이러한 제약조건 속에서 프로젝트를 성공적으로 구현하기 위해서는 성능의 미세한 조정이 선택이 아닌 필수입니다. - - 넉넉한 성능의 하드웨어를 가지고 있다면 이러한 중앙 모니터링의 중요성이 낮아질 수 있으나, 현재 상황으로서는 매우 중요한 위치를 가지고 있습니다. 또한 여러 시스템이 동시에 올라가 작동하는 네트워크 구조 상 디버깅을 위한 로그를 중앙에서 관리하는 것이 중요합니다. - - 거기에 일관된 서버 설정을 위하여 Matrix를 관리하는 것 뿐 아니라 자동화 된 도구가 필요했습니다. 이를 위하여 모니터링 및 로깅을 동시에 할 수 있는 시스템을 다음과 같이 선정하였습니다. - - 모니터링 및 로깅, 알람, 자동화 기능은 특별한 권한이 필요하거나 엄격한 논리적 분리가 필요한 기능이 아니므로 성능의 부하를 줄이기 위하여 LXC로 구축합니다. - - 추가 고려 사항 - - - - 각 서버의 상태를 한 곳에서 모니터링 할 수 있을 것 - - 각 서버의 로그를 한 곳에서 일괄 확인할 수 있을 것 - - 문제 발생 시 자동으로 알림을 보낼 수 있을 것 - - 각 서버의 설정을 자동화된 관리가 가능하게 할 것 - - - - 결론 - - 다음 고려 사항을 확인하였을 때, 이 부분은 실무 환경에서 가장 많이 흔히 사용하는 조합을 가져오기로 하였습니다. 홈 네트워크 구조 상 무시 받기 쉬운 로깅, 및 모니터링 시스템이지만 본 프로젝트의 목적인 경험과 학습, 그리고 미세한 성능 조정을 위하여 다음과 같은 시스템을 하나의 서버에서 운영하기로 결정했습니다. - - - - 모니터링: Prometheus (성능 모니터링 결과 수집)와 Grafana (대시보드 및 시각화) - - 로깅: Loki (수집 로그 관리)와 promtail (로그 수집) - - 알림: Alertmanager - - 서버 설정 자동화: Ansible + Semaphore (경량 Web UI) - - - - Proxy 및 DDNS - - 안정적인 웹 서비스 접근 및 SSO를 위하여, Reverse Proxy 및 SSO Proxy 관련 시스템을 하나로 묶어 구축하려고 합니다. 웹 요청이 있을 경우 Reverse Proxy는 SSO Proxy에게 인증 정보를 요청하고, SSO Proxy는 idP(identity Provider)를 통해(혹은 직접) Kerberos(혹은 다른 프로토콜)과 연동하여 인증 여부를 포함한 정보를 Reverse Proxy로 전달, 마지막으로 Reverse Proxy에서 요청을 웹 서비스로 전달하게 됩니다. DDNS는 WAN과 연결된 공인 IP를 탐지하여, 공인 IP 정보가 바뀌면 외부 DNS에 IP정보를 자동으로 전달하여 갱신 하는 역할을 합니다. - - DDNS에 경우 API(주로 DNS에서 제공)를 통한 간단한 스크립트로 구현이 가능하나, Reverse Proxy와 SSO Proxy(+idP)를 구현하기 위하여 주로 다음과 같은 시스템을 사용합니다.  - - Proxy server 역시 Monitoring server와 마찬가지로 특별한 권한이 필요하거나 엄격한 논리적 분리가 필요한 기능이 아니므로 성능의 부하를 줄이기 위하여 LXC로 구축합니다. - - 시스템 비교 - - - - - - - - 시스템 - - Apache HTTP server + mod_auth_gssapi - - Nginx + oauth2-proxy + keycloak(idP) - - Authentick - - - - - - - - - - 아키텍처 - - - - 단일형(+모듈) - - - - 분리형 - - 통합형 - - - - - - 라이센스 - - 무료 - - Open Source - - 무료(개인 사용자 한정) - - - - - - UI - - 설정 파일 외 관리 UI 부재 - - - - Nginx/oauth2-proxy: 설정 파일 - - Keycloak: Web UI 지원 - - - - Web UI 지원 - - - - - - - - 핵심 기능 - - - - - - Web, Reverse Proxy, Kerberos 직접 인증 및 위임 등 지원 - - - - - - Nginx: Reverse Proxy - - oauth2-proxy: OIDC/OAuth2, idP 등 지원 - - keycloak: Kerberos 인증, OIDC/SAML 토큰 발급, MFA 등 지원 - - - - - - idP(OIDC, SAML, LDAP), Kerberos 인증, 정책 엔진, - - 사용자/그룹 관리 지원, MFA, - - Application Proxy 등 지원 - - - - - - - - 설정 및 유지 보수 난이도 - - 중간 - - 높음 - - 높음 - - - - - - 최소 사양 - - 낮음 - - 중간 - - 중간 - - - - - - - - 추가 고려 사항 - - - - SSO라는 이해하기 힘든 개념에 대하여 대해 체계적으로 학습 가능하게 할 것 - - - - 결론 - - 단순한 구현이 목표라면 Apache를 사용하는 것이 가장 간단할 수 있을 것입니다. 하지만 프로젝트의 목표는 구현을 넘어 학습과 개념에 체득에 있습니다. - - 점점 SSO의 중요성이 부각되는 현대 사회에서 단순 Apache 사용은 앞으로 더더욱 입지가 줄어들 것입니다. 또한 Reverse Proxy와 SSO Proxy, 추가로 idP라는 복잡한 개념을 이해하려면 각각의 기능들을 이해할 수 있다면, 어떤 시스템을 사용한다고 하더라도 쉽게 익숙해지고 적응 할 것입니다. - - 또한 FreeIPA의 경우와 다르게, 이러한 기능은 십수가지가 아닌 3개로 나눠집니다. 따라서 SoC에 따른 분리를 하더라도 관리 포인트가 엄청나게 느는 것은 아닐 것으로 생각됩니다. - - 이런 고려사항을 따라 SoC에 따라 분리되어 있는  Nginx + oauth2-proxy + keycloack 을 선택해 적용하여 각 개념을 더욱 정밀하게 배울 수 있을 것으로 기대됩니다. 추후, 이러한 개념이 내재화 된다면, Authentick 같은 통합형 시스템도 쉽게 익숙해 질 수 있을 것을 기대합니다. - - 또한 DNS가 작동하는 FreeIPA가 아닌, Proxy Server에서 DDNS 스크립트가 작동하는 이유는 SoC 때문입니다. FreeIPA는 내부의 보안과 인증을 담당하는 서비스로 이러한 FreeIPA가 외부에 노출되는 것은 보안상 바람직하지 못합니다. 따라서, 외부와 통신하는 것이 주요한 역할인 Proxy에서 DDNS를 두어 SoC 원칙 하에 서비스를 분리하여 구축합니다. - - VPN - - WAN에서 안전하게 LAN으로 접근하여 서비스를 이용하고, 서버 및 클라이언트를 관리하기 위해서는 VPN이 필요합니다. VPN은 암호화된 터널을 구성해, WAN에서도 마치 특정 LAN에 속한 것 처럼 네트워크를 사용할 수 있도록 하는 기술입니다. - - 프로젝트에서 계획 중인 대부분의 서비스는 DDNS를 통한 외부 공개 접속이 가능하나, 특정 서비스(AdGuard Home, SSH, NFS, FTP 등)는 외부에 노출시 보안의 문제가 발생하기 쉽습니다. 따라서 이러한 특정 서비스를 안전하게 외부에서 이용하기 위해 VPN을 구축하여 사용하고자 합니다. - - 특히 OPNsense를 통한 VPN 서버를 구축 예정이므로, 공식적으로 지원하는 OpenVPN, WireGuard, IKEv2/IPSec을 비교하려 합니다. - - 시스템 비교 - - - - - - - - 시스템 - - OpenVPN - - WireGuard - - IKEv2/IPSec - - - - - - - - - - - -  호환성 - - (아키텍처) - - - - - - 다양함 - - (Windows, Linux, 모바일 등) - - - - - - 다양함 - - (Windows, Linux, 모바일 등) - - - - - - 대부분의 모바일 플랫폼 - - (RFC 프로토콜) - - - - - - - - 라이센스 - - Open Source - - Open Source - - - - Microsoft/Cisco - - - - - - - - 속도 - - 보통 - - 매우 빠름 - - 빠름 - - - - - - - -   - - 핵심 기능 - - - - 높은 배터리 소모, 높은 보안성과 안정성, 네트워크 전환 시 느린 재연결, 공식 앱 지원, 서드 파티 분할 터널링 지원 - - 낮은 배터리 소모, 높은 보안성과 안정성, 네트워크 전환 시 즉각적인 재연결, 공식 앱 지원, 공식 앱 분할 터널링 지원 - - 낮은 배터리 소모, 높은 보안성과 안정성, 네트워크 전환 시 즉각적인 재연결, OS 내장형, 분할 터널링 기능 지원하나 설정의 까다로움 - - - - - - - -   - - 설정 및  - - 유지 보수 난이도 - - - - - - 보통 - - (인증서 관리 필수, 수동으로 클라이언트 설정) - -   - - - - - - 매우 쉬움 - - (직관적인 UI, QR 코드를 통한 클라이언트 설정 가능) - - - - - - 어려움 - - (두 단계의 설정 및 일치, - - 벤더간 호환성 문제 발생 쉬움) - - - - - - - - - - 결론 - -  VPN을 사용할 때 있어서 중요한 것은 높은 보안성과 안정성 외에도 편의성도 큰 비중을 차지합니다. 특히 모바일 기기를 통한 VPN을 주로 사용하는 환경에서는 낮은 배터리 소모와 분할 터널링, 쉽고 직관적인 클라이언트 설정이 사용 경험을 크게 올려줄 것입니다. 따라서 세 프로토콜 전부 높은 보안성과 안정성을 보장할 수 있으므로, 배터리 소모량이 가장 적고 직관적이며 설정이 쉬운 WireGuard 를 선택했습니다. - - 파일 및 백업 관리와 클라우드 서비스 - - On-premise 환경을 구축하는 데 있어 파일 및 백업 관리와 클라우드 서비스는 가장 중요한 기능 중 하나입니다. 특히 빠른 I/O 속도가 상대적으로 중요하지 않은 대용량 파일(미디어 파일, 개인 파일, 백업 파일 등)은 SSD가 아닌 비용 효율적인 HDD에 저장하는 것이 가장 이상적입니다. - - 하지만 모든 서버에 HDD를 직접 마운트하는 방식은 복잡하고 비효율적입니다. 따라서, 중앙에 파일 서버를 구축하여 NFS 및 백업 서비스, 클라우드 서비스를 제공하는 것이 더욱 효율적입니다. - - 이때 Hypervisor로 부터 독립적인 파일 시스템을 관리하기 위하여 LXC가 아닌 VM으로 구축합니다. 이러한 중앙 통합적인 파일 서버를 구축하기 위하여 다음과 같은 사항들을 고려했습니다. - - 시스템 비교 - - OS - - - - - - - - 시스템 - - - - NAS OS (TrueNAS, OMV 등) - - - - Ubuntu Server - - Debian (Stable) - - - - - - - - - - 아키텍처 - - - - Unix/Linux 등 - - - - Linux - - Linux - - - - - - 라이센스 - - OS 별로 다름 - - Open Source - - Open Source - - - - - - UI - - 전문 GUI 지원 - - - - CLI 지원 - - - - CLI 지원 - - - - - - - - 핵심 기능 - - - - - - 파일 서버에 필요한 모든 기능들을 통합적으로 지원 - - (SMB, NFS, 인증, 백업, 미디어 서버, DDNS, 자체 클라우드 등) - - - - - - 필요한 프로토콜 및 기능 별도로 구현 - - - - - - 필요한 프로토콜 및 기능 별도로 구현 - - - - - - - - 설정 및 유지 보수 난이도 - - - - 낮음 - - (타 서버/서비스 연동시 높음) - - - - 중간 - - 낮음~중간 - - - - - - 최소 사양 - - 중간 - - 낮음 - - 낮음 - - - - - - - - Cloud service - - - - - - - - 시스템 - - - - NAS OS의 패키지 - - - - NextCloud - - Seafile - - - - - - - - - - 아키텍처 - - SW - - SW - - SW - - - - - - 라이센스 - - OS 별로 다름 - - Open Source - - 무료/유료 버전 - - - - - - UI - - 전문 GUI 지원 - - - - Web GUI 지원 - - - - Web GUI 지원 - - - - - - - - 핵심 기능 - - - - - - OS 별 차이가 있을 수 있으나 기본적으로 모든 기능을 포함한 통합 파일 관리 솔루션 제공 - - - - - - 파일 동기화, PIM 기능(캘린더, 할일, 주소록 등), media/office 기능을 포함한 통합 파일 관리 솔루션, LDAP/SSO 지원 - - - - 파일 동기화 및 관리/공유 기능 특화, LDAP/SSO 지원 - -   - - - - - - - - 설정 및 유지 보수 난이도 - - - - 낮음 - - (타 서버/서비스 연동시 높음) - - - - 중간 - - 중간 - - - - - - 최소 사양 - - 높음 - - 중간 - - 낮음 - - - - - - - - Backup service - - - - - - - - 시스템 - - - - NAS OS의 패키지 - - - - duplicati - - BorgBackup (+Vorta) - - Kopia - - - - - - - - - - 아키텍처 - - - - SW - - - - SW - - SW - - SW - - - - - - 라이센스 - - OS 별로 다름 - - Open Source - - Open Source - - Open Source - - - - - - UI - - 전문 GUI 지원 - - CLI, Web GUI 지원 - - - - CLI(기본), - - GUI(vorta) 지원 - - - - CLI, GUI 지원 - - - - - - - - 핵심 기능 - - - - - - OS 별 차이가 있을 수 있으나 기본적으로 스냅샷,클라우드 동기화/백업, 중복 제거 증분 백업, E2EE 백업, 스케줄 백업 등 제공 - - - - - - 다양한 클라우드 백업 등 백엔드 직접 지원, E2EE 암호화, 자동 스케줄링, 버전 관리 등 제공 - - - - - - 최고 수준 중복제거/압축, 강력한 E2EE 암호화, 스냅샷(아카이브) 기반, 스케줄 백업, 강력한 CLI 등 제공 - - - - 최신 기술(CDC 중복제거), 다양한 클라우드 백업 직접 지원, 정책 기반 관리, E2EE 암호화, 스케줄 백업 등 제공 - - - - - - - - 설정 및 - - 유지 보수 난이도 - - - - - - 낮음 - - (타 서버/서비스 연동시 높음) - - - - - - 낮음 - - - - - - 중간(CLI 사용 시 높음) - - - - - - 낮음 - - - - - - - - 최소 사양 - - 높음 - - - -  낮음 - - - - - - 낮음~중간 - - - - - -  중간 - - - - - - - - - - 추가 고려 사항 - - - - 타 서비스(OPNsense, FreeIPA, Proxy 등)과 연동 용이할 것 - - 다양한 기능보다는 File Server 자체 기능의 집중할 것 - - - - 결론 - - 프로젝트 진행에 있어 가장 중요한 점은 학습 가능성입니다. 존재하는 NAS OS를 그대로 파일 서버로 사용한다면 직접 설정하는 것보다는 편리할 수 있습니다. 하지만 다른 서비스들과의 통합과 부족한 하드웨어 성능, 그리고 학습 가능성을 고려했을 때 NAS OS 보다는 파일 서버를 직접 구축하는 것이 낫다는 결론을 내렸습니다. - - 따라서 서버를 구축하기 위한 OS로 NAS OS를 제외한 Ubuntu와 Debian(stable)을 비교하였습니다. Ubuntu와 Debian모두 안정적이고 가벼운 OS이나 Hypervisor인 PVE의 기반이 Debian이므로 다른 LXC와 VM 간 일관성을 위하여 Debian 을 선택하였습니다. - - 이때 기본 파일 시스템을 Btrfs로 적용하여 사용하려 하는데, Btrfs는 자체 중복 처리 기능과 CoW 기능, 그리고 스냅샷 기능을 제공합니다. 다만 CoW로 인한 외부 단편화가 발생할 수 있으므로 주기적인 디스크 압축 기능을 사용하여야 합니다.  - - 개인 파일 관리를 위한 Cloud Service를 위한 시스템을 선정하는데 있어서 가장 중요한 점은 필요 성능이 낮아야 한다는 것과 특화된 기능입니다. 파일 관리를 제외한 나머지 기능들, 즉 PIM 및 Multimedia 기능 등은 필요시 별도로 Docker를 통해 Application Server에 구축될 예정이기 때문입니다. 따라서 NextCloud와 Seafile을 비교하였을 때, 다양한 기능을 가진 NextCloud 보다 본연의 파일 관리 기능에 특화되어 있는 Seafile 을 선택하였습니다. - - 마지막으로 File Server의 파일들을 보존하기 위한 Backup Service를 위하여 고려한 점 역시 학습 가능성과 사용 편의성이 었습니다. - - Duplicati는 오랜 기간동안 쓰여온 파일 백업 시스템이지만, 최신 중복 제거 알고리즘 지원등이 다른 서비스보다 부족하였고 타 시스템보다 CLI를 통한 강력한 기능 지원이 부족하였습니다. - - BorgBackup 역시 오랜 기간 동안 쓰여온 강력한 시스템이며, 가장 강력하고 최신인 중복 제거 알고리즘을 사용합니다. 또, CLI를 통한 세밀한 관리 등이 가능하였으나, Vorta라는 별도의 GUI 시스템을 사용하지 않으면 GUI를 지원하지 않고 백엔트 연동 등의 편의성 부분에서 부족하였습니다. - - Kopia는 출시된 지 오래된 시스템은 아니지만 빠르게 발전하는 시스템입니다. 최신 중복 제거 알고리즘을 지원하며, CLI와 GUI 모두 지원하였습니다. 또한, 다양한 백엔드 시스템(타 사 클라우드 서비스 포함) 연동 기능을 자체적으로 보유하고 있습니다. - - 다음과 같은 점들을 모두 고려 하였을 때  Kopia 가 학습 가능성과 사용 편의성을 모두 충족시키는 것이라 생각되어 선택하였습니다. - - 이 경우, Seafile과 Kopia 모두 일관된 환경에서 실행하기 위하여 Docker로 배포합니다. - - 따라서 File Server는 Debian(Stable - netist) 위에서  Seafile + Kopia(on Docker) 을 구축하기로 운영하기로 했습니다.  - - DB, Web, Application 서비스 - - DB, Web, Application 서비스는 VM 위에서 Docker를 통하여 어느 환경에서나 동일한 작동을 보장하도록 구축할 것입니다. Docker의 경우 편리하지만, rootless docker는 사용에 있어 큰 제약(내부 인터페이스 생성 등)이 있기에 root 권한으로 실행되어야 합니다. - - 따라서 Docker를 사용하기 위하여서는 LXC가 아닌 VM을 사용하여야 합니다. 이는 LXC 상에서 Docker가 실행되었을 때 root 권한을 통해 LXC를 넘어 Hypervisor의 커널 영역까지 침범할 수 있는 위험성을 가지고 있기 때문입니다. LXC의 경우 appArmor/seccomp의 존재로 인하여 충분한 권한을 얻지 못한 Docker 서비스에 오류가 발생할 가능성이 큽니다. unprivileged LXC와 rootless docker의 경우 앞서 언급한 제약으로 인해 문제가 발생할 가능성이 있으므로 역시 권장되는 사용법이 아닙니다. 따라서 Docker 활용시에는 반드시 VM을 통한 완벽한 논리적 분리가 필요합니다. - - 따라서 앞선 File server의 경우처럼 VM을 통해 Proxmox VE와 일관성을 유지하기 위한 Debian(Stable - netist) 환경에서 Docker 서비스를 배포하는 형식으로 구현하고자 합니다. 구현하고자 하는 서비스의 목록은 다음과 같습니다. - - DB - - - - Maria DB, Postgre SQL 등 - DBMS - - - - Web - - - - Hompage - 홈페이지 및 대쉬보드 - - Ghost - 블로그 서비스 - - Bookstack - 개인 wiki, 노트 - - Gitea - Git 버전 관리 - - - - Application - - - - Portainer - 도커 통합 관리 - - Uptime Kuma - 도커 서비스 모니터링 - - Diun - 도커 업데이트 관리(수동 업데이트) - - n8n - 워크 플로우 자동화 - - Redis - 캐시 DB - - Code-Server - 웹 코드 에디터(Git 연동) - - Immich - 사진 및 개인 동영상 관리 - - PeerTube - 동영상 컨텐츠 관리 및 스트리밍 - - Navidrome - 음악 관리 - - Vaultwarden - 개인 암호 관리 - - Infisical - DB, API key 등 관리 - - Matrix-Synapse/element - 채팅/통화/영상 통화 - - Radicale - 캘린더 및 주소록 - - Vikunja - To-DO 리스트 - - Firefly III - 가계부 - - Paperless-ngx - 문서 관리 - - Snipe-IT - ITAM; IT 자산 관리 - - Calibre-web - 전자책 관리 - - Kmoga - 만화책 관리 - - Audiobookshelf - 오디오 북 관리 - - - - 보안 실습 환경 - - 보안 실습 환경은 Kali linux와 Practice server로 나누어 운영를 할 것입니다. 두 서버 모두 보안 실습에 활용될 예정이므로 Hypervisor에 영향을 줄 수 없도록 VM으로 격리하여야 합니다. Kali는 보안 실습에 필요한 많은 도구들을 자체적으로 내장하고 있는 OS이며, Practice server는 목적에 맞게 여러 OS를 활용할 계획입니다. Windows 부터 Unix, Linux 등을 필요할 때 설치하고, 실습 완료 후 제거하는 방식으로 운영할 것입니다. - - 결론 - - SoC 원칙 및 VM/LXC/Docker 환경 내에서 주의하여야 할 점들을 명확하게 하여 필요한 서비스들을 정리 및 시스템을 선정하였습니다. 이러한 필요 서비스들을 한 번에 구축하는 것이 아닌, 중요 순서 별로 차례 차례 구축하며 할당 성능을 조절한다면 주어진 물리적 제약 조건 속에서도 목표하는 모든 사항을 구현할 수 있을 것으로 보입니다. - - 최종적인 시스템 선정 결과는  1.3. 최종 목표 아키텍처 에 정리되어 있습니다. - - - - 2025-05-29 - 초안 작성 중 - - 2025-06-04 - 초안 작성 완료 - - 2025-06-16 - AdGuard Home LXC 내용 추가 - - 2025-06-17 - VPN 내용 추가 및 부록 1.1. 문서 작성 가이드라인 에 따라 본문 재작성 - - 2025-06-19 - Rocky Linux 내용 추가 - - 2025-06-20 - 날짜 표기 변경 - -2.4. 보안 설계 -목표 - - - - 네트워크, 시스템, 인증, 및 정책 모든 부분에 대한 잠재적인 보안 위협을 예상하고, 이를 효율적으로 차단할 수 있는 방안을 구상합니다. - - 각 과정의 고려사항 및 근거를 명확하게 기록합니다. - - - - 요구 사항 - - - - 최소 권한 원칙 및 Zero Trust 원칙 하, 보안 위험을 식별하고 차단합니다. - - T5004/AX6000M/OPNsense/FreeIPA 등의 네트워크 관련 정책을 설계합니다. - - 방화벽, IPS/IDS, Port Fowarding 및 NAT, VLAN, VPN 등이 포함합니다. - - 각 서버 별 공통으로 적용되어야 할 보안 규칙(System Hardening)과 예외 사항을 작성합니다. - - ACL을 위한 UID/GID 관리와 FreeIPA를 통한 SSO 규칙을 작성합니다. - - 데이터 보안 정책을 설계합니다. - - - - 검토 사항 - - 네트워크 보안 정책 설계 - - T5004/AX6000M 보안 정책 - - 관리자 접근 - - - - WAN에서의 관리자 페이지 접속을 원천적으로 차단 - - LAN 혹은 VPN을 통해서만 접근 허용 - - - - Wi-Fi 보안 - - - - 무선 네트워크에 대하여 WPA3 암호화 방식 사용 - - 강력한 비밀번호 설정 - - IoT 기기와 Client의 IP 대역 분리 - - - - OPNsense 상에서 IoT 기기의 IP 대역에 대하여 다음 규칙 적용 - - Allow: 목적지가 AdGuard Home의 DNS 포트인 트래픽과 WAN인 트래픽 - - Dney: 모든 내부망 트래픽 - - Clients는 전부 각각 Static IP 주소 할당하여, 대역이 아닌 IP 주소로 접근 제어 - - IP 위변조시 IP 주소 충돌로 확인 가능 , WTR Pro 외부 VLAN 적용 불가인 현 환경에서 최선 - - - - - - - - 펌웨어 관리 - - - - 주기적으로 최신 펌웨어 확인 및 유지 - - - - 취약 기능 비활성화 - - - - UPnP와 같은 보안에 취약한 기능은 비활성화 원칙 - - - - 방화벽 정책 - - 기본 정책(Default Deny) - - - - 모든 인터페이스 간 통신은 기본적으로 차단 - - - - VLAN 간 통신 정책 - - - - 특정 Client만 SSH등 접속 허용 - - 그 외 모든 통신은 원칙적으로 Proxy Server의 https 포트와, DNS 통신만 허용 - - - - CrowdSec - - - - 각 서버에 별개로 Fail2Ban을 설정하지 않고 중앙 OPNsense에서 CrowdSec 플러그인을 통해 중앙 관제 및 대응 - - 커뮤니티 기반의 악성 IP 차단 목록 활성화하여 알려진 공격자 차단 - - 모든 VM/LXC에 Ansible을 기반으로 CrowdSec 에이전트를 설치하여 OPNsense에서 통합 분석 - - - - IPS/IDS 정책 - - 초기 운영 정책 - - - - 운영 초기 서버 부하를 알기 위하여 IDS 모드로 운영하여 충분히 검토 - - 이후 점진적인 IPS 규칙 적용 - - - - 규칙 셋 - - - - 커뮤니티 기반 ET Open 규칙셋을 적용 - - 최대한 가벼운 체험 목적의 운영 - - 오탐 규칙은 개별적인 비활성화 - - - - Port Fowarding 및 NAT 정책 - - - - INBOUND 접근은 https(tcp/443)과 WireGuard VPN의 포트만 허용 - - T5004는 모든 트래픽을 OPNsense로 전달(Port Fowarding) - - - - DNS 정책 - - - - 기본 DNS를 AdGuard Home으로 설정 - - AdGuard Home은 광고 Domain을 차단 후 FreeIPA DNS로 upstream - - FreeIPA는 Spilt Horizon DNS를 구현, 매치되지 않는 Domain에 대하여 Public DNS로 Fowarding - - - - VPN 접속 정책 - - VPN 프로토콜 - - - - WireGuard 프로토콜 사용 - - - - VPN Server - - - - OPNsense에서 구동 - - - - 접근 원칙 - - - - 외부에서 내부 관리 기능(SSH, PVE 웹 UI, Reverse Proxy 등) 이용시 반드시 VPN을 통해 접속 - - https 접근이 아닌 모든 프로토콜(RDP, NFS, SMB 등)은 반드시 VPN을 통해 접속 - - - - 시스템 보안 정책 설계 - - root 계정 보안 - - - - 명시적인 ssh root 접속 차단 - - 서버 관리용 계정 생성 - - root 권한 필요시 sudo group을 활용한 sudo 사용 - - sudo 사용 기록 로깅 - - - - 시스템 하드닝(System hardening) 정책 - - 자동화 관리 - - Ansible 플레이북을 통해 신규 VM/LXC 배포시 다음 정책 강제 적용 - - - - 최소 패키지 원칙 - - 정기적 업데이트 - - 로컬 방화벽(iptables) 정책 - - - - 최소 권한 원칙 적용 - - ssh 등 필수 포트 개방 - - 추가 포트 개방 필요시 수동으로 정의 - - - - - - CrowdSec 에이전트 설치하여 침입 탐지 및 공격 차단 - - - - Secrets 관리 정책 - - Infisical Secret - - - - Infisical이 자신의 DB에 접속하기 위한 초기 비밀번호는 Ansible Vault를 통해 암호화 - - Vault 마스터 비밀번호는 개인 암호 관리자(Vaultwarden)에 보관 - - - - Infisical 정책 - - - - Infisical 실행 이후 DB 접속 정보, API 키 등 모든 Secret은 Infisical를 통해 중앙 집중식 관리 - - 주요 API 키 및 DB 비밀번호는 주기적인 변경 - - - - 개인 Secret 관리 - - - - Vaultwarden 서버를 활용하여 저장 - - - - 인증 및 접근 제어 정책 설계 - - FreeIAP를 통한 SSO 정책 - - Application 인증 - - - - BookStack, Gitea 등 SSO를 지원하는 서비스의 사용자 로그인은 FreeIPA를 통한 LDAP/SSO를 통해 적용 - - 직접 SSO가 지원되지 않는 서비스는 idP를 이용하여 SSO 적용 - - - - Server 인증 - - - - 모든 서버의 SSH 접속은 비밀번호가 아닌, FreeIPA를 통한 Kerberos 혹은 SSH 공개키 인증 방식 적용 - - - - 계정 및 권한 정책 - - Local UID/GID 정책 - - - - 로컬 UID/GID 대역: 2000~9999 - - - - 서버 자체의 root를 대신할 관리자 UID:GID - - ACL 관리는 FreeIPA가 LDAP로 담당 - - - - - - 서버별 대표 UID:GID 설정(예시) - - - - 서버 관리 그룹 GID: 2000  - - DB 서버의 대표 UID: 2001 - - 파일 서버의 대표 UID: 2002 - - - - - - FreeIPA에 할당될 LDAP/SSO 대역: 10000~ - - - - 최소 권한 원칙 - - - - 원격 root 접속은 제한 - - 모든 사용자는 일반 권한 보유 - - 관리자 권한 필요 작업시 sudo 사용 후 사용 내역 로깅 - - - - DB 정책 - - 접속 호스트 제한 - - - - 각 user는 자신의 DB에 모든 권한 보유 - - DB 로그인은 'user'@'ip_address' 형태로 해당 계정을 사용하는 서비스의 IP 주소에서만 접근 가능하도록 명시적 제한 - - - - 데이터 보안 정책 - - 파일 시스템 - - - - Btrfs 사용하여 CoW, 스냅샷 등 고급 기능 활성화 - - - - DB의 논리적 백업 - - - - 모든 DB는 pg_dump (PostgreSQL) 혹은 mysqldump (MariaDB) 사용하여 논리적 백업 수행 - - 백업 수행 결과는 File Server로 이동하여 저장 - - - - 데이터 백업 - - - - 파일 서버의 데이터는 Kopia를 사용하여 DS124에 1차 백업 - - 주요 데이터는 E2EE를 적용하여 클라우드 백업 - - - - 결론 - - 보안 분야는 무궁무진하므로 새로운 위험과 취약점을 발견 시 언제든 수정될 수 있습니다. 고정된 설계가 아닌 프로젝트가 진행 되며 같이 변경되는 설계로 보안 분야에 대한 학습과 이해, 그리고 정책 수립 등에 익숙해지고자 합니다. - - 최종적인 보안 설계 구조는 1.3. 최종 목표 아키텍처 에 정리되어 있습니다. - - - - 2025-06-08 - 초안 작성 - - 2025-06-19 - 세부 사항 작성 - - 2025-06-20 - 날짜 표기 변경 - -2.5. 구현 계획 -목표 - - - - 실제로 프로젝트를 구현하기 앞서 구체적인 구현 순서를 계획합니다. - - 계획에 따라 순차적으로 서비스를 구현하고 검증합니다. - - - - 구현 계획 - - 네트워크 장비 설정 - - 기보유 네트워크 장비들을 계획에 맞도록 설정합니다. - - IP Matrix 작성 - - 네트워크 장비 설정에 앞서, 각 네트워크 장비와 서버 및 클라이언트의 IP 대역을 미리 계획합니다. 이를 Matrix 형태로 정리하여 추후 관리 및 유지보수가 편리하도록 합니다. - - T5004/AX6000M 설정 - - 현 네트워크 구조의 핵심인 T5004와, AX6000M의 설정을 합니다. 필요 설정은 크게 다음과 같습니다. - - - - DHCP 서버 설정(기본 게이트웨이, DNS, Static/Dynamic IP 범위 등) - - 보안 설정(NAT 설정, UPnP 설정, 라우터 WAN 접근 차단, 관리자 ID/PW 설정, 접속 국가 차단, 무선 LAN 설정 등) - - 펌웨어 업데이트 확인 - - 물리적인 보안 대책 확인 - - - - PVE 설정 - - VM/LXC를 통한 각 서버 구현 전 Hypervisor인 Proxmox VE를 설치하고 설정합니다. - - UID/GID Matrix 작성 - - 앞으로 구현될 각 서버의 UID/GID 대역을 설정합니다. FreeIPA 설정 후 통합될 LDAP/SSO 전용 대역과 로컬 UID/GID를 분리합니다. - - PVE 설치 - - 임시 서비스 Debain(Stable - netist) 설정 - - Bookstack, Gitea 등 프로젝트 진행을 위하여 필수적인 서비스를 임시로 설치합니다. 추후 WTR Pro에 서비스 구현시 마이그레이션합니다. - - 필수 설치 패키지 목록 - - - - curl - - sudo - - dockerd - - - - 임시 서비스 목록 - - - - nginx-proxy-manager[jc21/nginx-proxy-manager] - - MariaDB[mariadb] - - BookStack[linuxserver/bookstack] - - Gitea[gitea/gitea] - - Code-Server - - PostgreSQL[postgres] - - redis[redis] - - Infisical[infisical/infisical] - - immich - - - - OPNsense 설정 - - Monitoring Server 설정 - - AdGuard Home 설정 - - FreeIPA 설정 - - Proxy Server 설정 - - DB Server 설정 - - 기존 서비스의 Local DB 마이그레이션 - - File Server 설정 - - Web Server/WAS 설정 - - Application Server 설정 - - 정보보안 실습환경 설정 - - 결론 - - 이 계획 문서는 프로젝트의 진행에 따라 계속해서 갱신됩니다. 현재 프로젝트 구현의 큰 흐름이 잡힌 상태입니다. 이러한 계획에 따라 프로젝트를 구현합니다. - - - - 2025-06-07 - 초안 작성 - - 2025-06-16 - AdGuard Home 내용 추가 - - 2025-06-17 - DS124 상의 임시 서비스 내용 추가 - - 2025-06-19 - DS124 상의 임시 서비스 내용 삭제 및 Debian (stable - netits) 상의 임시 서비스 내용 추가 - - 2025-06-20 - 날짜 표기 변경 - -3. 네트워크 장비 설정 - -3.1. IP Matrix 작성 -목표 - - - - 추후 구현될 Server들이 속할 VLAN과 IP 주소 대역을 깔끔하고 보기 쉽게 관리합니다. - - On-premise 환경을 구현하기 앞서 VLAN과 IP를 명확하게 Matrix로 작성합니다. - - - - 요구 사항 - - - - VLAN Matirx 작성 - - 각 VLAN 별 IP Matrix 작성 - - - - Matrix - - VLAN - - - - - - - - VLAN ID - - VLAN 이름 - - IP 대역(CIDR) - - Gateway - - DHCP 범위 - - 역할/설명 - - - - - - - - - - Untagged VLAN - - default LAN - - 192.168.0.0/24 - - - - 192.168.0.3 - - - - 192.168.0.50~254 - - WTR Pro 외부 Clients - - - - - - 2 - - Server - - 192.168.2.0/24 - - 192.168.2.1 - - N/A - - - - 서버 및 - - 관리 인프라 - - - - - - - - 3 - - Kali - - 192.168.3.0/24 - - - - 192.168.3.1 - - - - N/A - - 정보보호 실습 - - - - - - 4 - - Practice - - 192.168.4.0/24 - - 192.168.4.1 - - N/A - - 정보보호 실습 - - - - - - 10 - - VPN - - 10.0.0.0/24 - - 10.0.0.1 - - 10.0.0.2~254 - - VPN 할당 대역 - - - - - - - - Static IP Address - - Untagged VLAN - - - - - - - - 장치/서비스 - - 이름 - - IP 주소 - - MAC 주소 - - 주요 서비스 및 포트 - - 접근 정책 - - 비고 - - - - - - - - - - - - T5004 - - router - - - - ISP 할당 WAN 주소 - - 192.168.0.1 - - - - - - 물리 NIC - - [WAN: 58-86-94-96-BF-E9] - - [LAN: 58:86:94:96:BF:E8] - - - - WAN gateway - - - - Inbound/Outbound 트래픽 제어, NAT 수행 - - - - - - - - - - - - OPNsense - - opnsense-vm - - - - 192.168.0.2(vtnet0) - - 192.168.0.3(vtnet1) - - - - - - vmbr0,1 - - [NIC0: C8:FF:BF:05:AA:B0] - - [NIC1: C8:FF:BF:05:AA:B1] - - - - - - Firewall, LAN gateway, DHCP, VPN Server, IPS/IDS - - - - Inbound/Outbound 트래픽 제어, 내부 라우팅, 중앙 방화벽 - - - - - - - - - - AX6000M - - ap - - - - 192.168.0.4 - - - - - -  물리 NIC - - [58:86:94:43:BC:8F] - - - - AP - - AP - - - - - - - - - - Temporary PVE - - pve - - - - 192.168.0.48 - - (OPNsense 구축 전) - - - - - - vmbr1 - - [NIC1: C8:FF:BF:05:AA:B1] - - - - PVE Web(tmp) - - 임시, Console에서만 접근 가능 - - - - - - - - - - NAS - - ds124 - - 192.168.0.5 - - - - 물리 NIC - - [90:09:D0:65:A9:DB] - - - - Backup/File server - - Untagged VLAN, VLAN2 접근 가능 - - - - - - - - - - PC1 - - pc-1 - - 192.168.0.6 - - - - 물리 NIC - - [CC:28:AA:A7:D9:33]  - - - - Client/Console - - 모든 VLAN 접근 가능 - - - - - - - - - - PC2 - - pc-2 - - 192.168.0.7 - - - - 물리 NIC - - [3C:7C:3F:D3:1B:D7] - - - - Client - - DNS, Proxy 접근 가능 - - - - - - - - - - Peinter - - printer - - - - 192.168.0.8 - - - - - - - - 물리 NIC - - [38:CA:84:94:5E:07] - - - - Client - - DNS, Proxy 접근 가능 - - - - - - - - - - TV - - TV - - - - 192.168.0.9 - - - - - - 물리 NIC [7C:0A:3F:3D:12:37] - - - - Client - - DNS, Proxy 접근 가능 - - - - - - - - - - Temporary Server - - temp-app - - - - 192.168.0.49 - - (임시 서비스 서버) - - - - - - vmbr1 - - [가상 eth: BC:24:11:ED:10:79] - - - - docker host - - - - 임시, Console 및 Web 접근 가능, - - SSH는 Console 만 가능 - - - - - - - - - - - - ↳Docker Services - - containers - - - - 172.16.0.x - - - - docker bridge - - N/A - - Reverse Proxy 접근 통해 가능 - - - - - - - - - - - - VLAN2 - - VLAN3 - - VLAN4 - - VLAN10 - - 결론 - - 현재 임시 환경 Matrix 작성 중입니다. 추후 서버 및 정보보호 환경, VPN 환경 구축 시 Matrix 업데이트 예정입니다. - - - - 2025-06-19 - 초안 작성 - - 2025-06-20 - IP Matrix MAC 주소 추가 - - 2025-06-23 - VLAN 표기 방식 수정 - -3.2. T5004/AX6000M 설정 -목표 - - - - 현 집안 환경의 물리적 네트워크 구조 핵심인 T5004와 AX6000M을 설정합니다. - - - - 요구 사항 - - - - T5004의 초기 설정을 진행합니다. - - T5004의 DHCP 서버를 설정합니다. - - T5004와 AX6000M의 보안 설정을 합니다. - - - - 포트 포워딩 설정 - - UPnP 설정 - - 라우터 관리 페이지 WAN 접근 차단 설정 - - 관리자 ID/PW 설정 - - 접속 국가 차단 설정 - - 무선 LAN 설정 - - - - - - 펌웨어 업데이트를 진행합니다. - - 설정을 백업합니다. - - 물리적인 보안 대책을 확인합니다. - - - - 수행 작업 - - 초기 설정 - - 시스템 관리 - - 관리자 설정 - - - - 관리자 계정 설정 - - 로그인 인증 방법 - - - - 세션 방식 - - 10분간 미사용 시 자동 로그아웃 - - Captcha 활성화 - - - - - - - - 펌웨어 업그레이드 - - - - 자동 업그레이드 수행 - - 월 1회 주기적인 재확인 - - - - 기타 설정 - - - - 공유기 관리 포트 - - - - 기본 http 지정 - - - - - - UPnP 설정 끄기 - - - - 특수 기능 - - IPTV 설정 - - - - IPTV 사용 안함 - - - - DHCP 서버 설정 - - 네트워크 관리 - - 인터넷 설정 정보 - - - - WAN 정보(ISP 기본 설정) 확인 - - DNS 주소 확인 - - - - DNS 주소는 AdGuard Home 구축 후 변경 - - - - - - WAN MAC 주소 확인 - - - - 내부 네트워크 설정 - - - - 내부 IP 주소(T5004) 확인 - - LAN Subnetmask 확인 - - - - DHCP 서버 설정 - - - - DHCP 실행 - - DHCP 설정 - - - - 대여 범위(Dynamic IP range): 192.168.0.50 ~ 192.168.0.254 - - Subnetmask: 255.255.255.0 - - Gateway: 192.168.0.1 - - - - OPNsense 구축 후 192.168.0.3으로 변경 - - OPNsense의 Gateway는 192.168.0.1로, vtnet0(192.168.0.2) 통해서 나감  - - - - - -  DNS 주소 - - - - AdGuard Home 구축 후 변경 - - - - - - - - - - 작성한 IP Matric를 바탕으로 Static  IP 할당 - - - - 등록된 주소 관리 + 버튼 클릭 - - IP 주소, MAC 주소, 설명 추가 - - - - - - IP 재할당을 위하여 재시작 - - - - Easy Mesh 관리툴 - - Easy Mesh 기본 설정 - - - - Controller mode 설정 - - - - Controller mode 네트워크 이름 및 암호 지정 - - - - 인증방법 WPA3SAE/WPA2PSK + AES - - 이름 알림 비활성화 - - - - - - - - - - WiFi 설정 - - - - 2.4GHz, 5GHz SSID 개별 설정 - - - - 인증방법 WPA3SAE/WPA2PSK + AES - - 이름 알림 활성화 - - - - - -  6GHz 비활성화 - - 비밀번호 설정 - - 게스트 네트워크 사용 비활성화 - - - - AdGuard Home 사용시 필요 - - - - - - - - - - - - Easy Mesh 고급 설정 - - - - 자동 재시작 - - - - 주 1회 05:00 순차적 재시작 - - - - - - - - Easy Mesh 관리 - - - - Easy Mesh 연결할 T5004와 AX6000M LAN 포트간 연결 후 Easy Mesh 관리툴 접속 - - 연결 가능한 ipTIME 건색 - - AX6000M 확인 후 IP 자동으로 받아오기 및 Agent 이름 지정 - - - - 설정 페이지 비활성화 - - 고급 설정 - 무선 백홀 사용 안함 - - - - - - - - 보안 설정 - - 보안기능 - - 공유기 접속/보안 관리 - - - - 원격 관리 포트 비활성화 - - 외부 접속 보안 비활성화 - - 내부 접속 보안 비활성화 - - - - 추후 Reverse Proxy 구축 시 Proxy 및 클라이언트 IP만 허용 - - - - - - - - 국가별 접속 제한 - - - - 국가 허용 - - - - 한국, 캐나다만 허용 - - - - - - - - NAT/라우터 관리 - - 포트포워드 설정 - - - - 추후 VPN 서버 구축시 VPN 포트 허용 - - https(443) 포트만 DS124로 포트포워딩 - - - - OPNsense 구축 시 OPNsense로 포트포워딩 - - - - - - - - 고급 NAT 설정 - - - - 인터넷 공유 기능 활성화 - - 포트포워드 UPnP 릴레이 비활성화 - - - - 설정 백업 - - 시스템 관리 - - 기타 설정 - - - - 설정 백업 복구 - - - - 공유기 백업 - - - - - - - - 물리적 보안 대책 - - - - 통신 단자함 내 T5004 위치 - - 거실 내 AX6000M 위치 - - 미사용 LAN 포트 봉인 - - - - 결론 - - - - 네트워크 연결 확인 - - Static IP 할당 확인 - - WiFi 설정 확인 - - 물리적 보안을 위한 미사용 LAN 포트 봉인 - - 추후 프로젝트 진행하며 업데이트 예정 - - - - - - 2025-06-20 - 초안 작성 - -4. PVE 설정 - -4.1. 서비스 UID/GID Matrix 작성 -목표 - - - - 프로젝트 내 모든 서버의 파일 권한을 일관되게 관리하기 위한 체계를 수립합니다. - - 서버 간 상호 작용을 위한 Local 계정의 대역과, 사람이 사용할 LDAP 계정의 대역을 명확하게 구별하고 대역을 분리합니다. - - 분리한 대역을 명확하게 Matrix로 작성합니다. - - - - 요구 사항 - - - - Local UID/GID 대역 정의 - - - - 각 서버 별 대표 UID/GID 정의 - - - - - - Docker의 UID/GID - - FreeIPA LDAP/SSO 전용 UID/GID 대역 정의 - - - - Matrix - - UID/GID 대역 - - - - - - - - 종류 - - Local Server - - FreeIPA LDAP/SSO - - Docker - - - - - - - - - - UID - - 2000~9999 - - 10000~ - - Container가 가진 기본 값 - - - - - - GID - - - - 2000~9999 - - *2000(svadmins), *9000(security) - - - - - - - - - - 대역 설명 - - - - 각 서버 관리 계정은 기본적으로 동일한(같은 숫자의) UID:GID를 갖는다. - - - - ex) 2006(dbsvadmin):2006(dbsv) - - - - - - 모든 서버 관리 계정은 2000 svadmins라는 추가 그룹에 속한다. - - Docker의 경우 Docker Container가 가진 기본 UID/GID를 사용한다. - - 도커 유저가 사용하는 파일 및 디렉토리는 기본적으로 [도커 컨테이너 내부 유저]:[호스트 서버 그룹] 형태이다. - - - - ex) 999(mariadb):2006(dbsv) - - - - - - 둘 이상의 서버가 공유하는 파일 및 디렉토리 만이 2000(svadmins)를 guid로 갖는다. - - - - (백업 디렉토리 등) - - - - - - 특정 서버와만 공유 하는 파일 및 디렉튜리는 해당 서버의 gid를 부여한다. - - - - (로그 파일 등) - - - - - - 모든 서버에 그룹 add를 통해 각 서버의 gid를 모두 정의한다. - - 추후 LDAP를 통한 파일 ACL 관리를 추가한다. - - - - Local UID/GID 정의 - - - - - - - - 서비스 - - UID - - GID - - 대표 GID - - - - - - - - - - PVE - - 2000(pveadmin) - - 2000(svadmins) - - 2000(svadmins, 대표) - - - - - - OPNsense - - 2001 - - 2001 - - - - - - Rocky Linux - FreeIPA - - 2002 - - 2002 - - - - - - AdGuard Home - - 2003 - - 2003 - - - - - - Monitoring Server - - 2004 - - 2004 - - - - - - Proxy Server - - 2005 - - 2005 - - - - - - DB Server - - 2006 - - 2006 - - - - - - File Server - - 2007 - - 2007 - - - - - - Web Server/WAS - - 2008 - - 2008 - - - - - - Application Server - - 2009 - - 2009 - - - - - - 임시 Debian Server - - 2999(temp-app) - - 2000(svadmins) - - - - - - Kali - - 9000 - - 9000(security) - - 9000(security) - - - - - - Pactice Client - - 9001~9999 - - 9001~9999 - - - - - - - - FreeIPA LDAP/SSO UID/GID 정의 - - - - - - - - 서비스 - - UID - - GID - - - - - - - - - - FreeIPA LDAP/SSO - - 10000~ - - 10000~ - - - - - - - - 결론 - - 프로젝트를 진행하며 LDAP/SSO에 부여할 UID와 GID를 세분화 하여 업데이트 예정입니다. - - - - 2025-06-21 - 초안 작성 - - 2026-06-27 - uid/gid 관리 방법 세분화 - -4.2. PVE 설치 -목표 - - - - PVE를 WTR Pro에 설치합니다. - - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - - - 요구 사항 - - - - PVE를 WTR Pro에 설치 - - PVE 원격 접속 - - PVE의 초기 설정 - - - - 수행 작업 - - PVE 설치 사전 준비 - - 설치 USB 준비 - - 프로젝트를 위하여 많은 OS 설치 파일이 필요합니다. 이러한 OS 설치 파일을 USB에 하나씩 넣기에는 비효율적입니다. 따라서 Ventoy라는 소프트웨어를 준비하여 하나의 USB로 여러 가지 OS 설치 파일을 한 번에 관리할 수 있도록 합니다. - - 사용 USB는 Samsung FIT Plus 64GB 입니다. - - Ventoy 설치 - - Ventoy는 오픈 소스로 여러 OS 설치 파일을 USB에 복사하여 부팅 가능하게 만들 수 있는 USB 드라이브 생성도구입니다. - - - - 공식 사이트: https://www.ventoy.net/en/download.html - - 파일을 다운 받은 후, Powershell에서  Get-FileHash -Path [file_path] -Algorithm SHA256 명령어를 통해 해시 값을 확인 - - 공식 사이트에 명시된 Hash값과 차이 없을 시 준비된 Ventoy USB에 ISO 파일 복사(무결성 확인) - - USB를 연결 후 Ventoy 파일을 실행하고,  Install 버튼을 눌러 USB에 Ventoy 설치 - - 이후 OS 설치 파일을 마운트 된 Ventoy Directory에 복사 - - - - PVE 설치 파일 준비 - - - - 공식 사이트: https://www.proxmox.com/en/downloads - - 파일을 다운 받은 후, Powershell에서  Get-FileHash -Path [file_path] -Algorithm SHA256 명령어를 통해 해시 값을 확인 - - 공식 사이트에 명시된 Hash값과 차이 없을 시 준비된 Ventoy USB에 ISO 파일 복사(무결성 확인) - - - - PVE 설치 - - WTR Pro 부팅 - - - - WTR Pro에 USB 연결 후 전원 켜기 - - Ventoy 화면에서 PVE 설치 파일을 선택 - - Boot in normal mode 선택 - - - - PVE 설치 - - - - Install Proxmox VE (Graphic) 선택 - - 약관 동의 - - 저장소 선택 (SSD, 기본 Ext4) - - 국가, 타임 존, 키보드 레이아웃 선택 - - - - South Korea - - Asia/Seoul - - U.S. English - - - - - - (root) 비밀번호, E-mail 지정 - - NIC 지정 - - - - Hostname(FQDN): pve.example.com - - IP(CIDR) - - Gateway/DNS: DHCP - - - - - - Summary 확인 후 Install 선택 - - CLI 상 https://[ip_address]:8006 확인 - - 브라우저 상에서 위 주소로 Proxmox Web UI 접속 - - - - 보안 경고시, 고급 -  안전하지 않음으로 이동 선택 - - - - - - - - PVE 초기 설정 - - PVE 로그인 - - - - User name: root - - Password: 설치 시 설정한 Password - - 언어: 한국어 - 국어 - - - - 사용자 이름 저장: 미체크 - - - - - - PVE Enterprise 라이센스 해지 - - Web UI 나 Shell 중에 하나 선택하여 라이센스 해지 - - - - Web UI - - - - 데이터센터 - pve - 업데이트 -  리포지토리 에서 다음 항목  비활성화 - - https://enterprise.proxmox.com/debian/ceph-quincy - - https://enterprise.proxmox.com/debian/pve - - - - - - Shell - - - - - - #!/bin/bash - -vi /etc/apt/sources.list.d/pve-enterprise.list - -#편집(주석 처리) - -#https://enterprise.proxmox.com/debian/ceph-quincy - -vi /etc/apt/sources.list.d/ceph.list - -#편집(주석 처리) - -#https://enterprise.proxmox.com/debian/pve - - - - - - - - - - 유효한 구독 없음 팝업 비활성화 - - - - Shell - - - - - - #!/bin/bash - -cd /usr/share/javascript/proxmox-widget-toolkit - -#파일 백업 - -cp proxmoxlib.js proxmoxlib.js.bak - -vi proxmoxlib.js - -#ESC+'/' 이후 No valid 검색 - -#.data.status.toLowerCase() !== 'active') 행을 - -#.data.status.toLowerCase() == 'active') 로 변경 - -systemctl restart pveproxy.service - -#서비스 재시작 - - - - 브라우저 Cache가 남아 있는 경우 계속 팝업이 활성화 될 수 있으므로 강화된 새로고침 필수 - - 추후 PVE 업데이트 시 다시 활성화 될 수 있으므로, 업데이트 후 재실행 - - - - - - - - 관리자 계정 생성 - - 계정 생성 - - - - Shell - - - - - - #!/bin/bash - -apt update - -apt install sudo -y - -apt update - -#sudo 명령어 설치 - -groupadd -g 2000 svadmins - -useradd -u 2000 -g 2000 -m -s /bin/bash -c "Proxmox Admin" -G sudo pveadmin - -#-m: 홈 디렉터리 자동 설정, -s: 셸, -c: 설명, -G sudo: sudo group 포함 - -passwd pveadmin - -#Password 설정 - -id pveadmin - -#결과 예시: uid=2000(pveadmin) gid=2000(pveadmins) groups=2000(pveadmins),27(sudo) - - - - - - - - Web UI - - - - 데이터센터 - 사용자 - 추가 - - - - 계정: pveadmin - - 영역: Linux PAM standard authentication - - email - - - - - - 비밀번호 - - - - 추가 - - - - - - 데이터센터 -  권한 - 추가 - 사용자 권한 - - - - 경로: /   - - 사용자: pveadmin - - 역할: administration - - - - - - - - - - - - root 계정 ssh 로그인 금지 - - - - Shell - - - - - - #!/bin/bash - -sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak - -sudo vi /etc/ssh/sshd_config - -#PermitRootLogin no로 변경 - -sudo service sshd restart - - - - - - - - Web UI - - - - 데이터센터 - 사용자 - root 선택 - 수정 - - - - 활성화 됨 체크 해제 - - - - - - - - - - - - iptables 설정 - - - - Shell - - - - - - #!/bin/bash - -sudo apt install iptables-persistent #debian iptables 저장 패키지 - -#Save current IPv4 Rulse 선택 - -# 명령어 설명 - -# 1. 모든 규칙 초기화 - -iptables -F - -iptables -X - -iptables -t nat -F - -iptables -t nat -X - -iptables -t mangle -F - -iptables -t mangle -X - -# 2. 기본 정책 설정 (Default-Drop) - -iptables -P INPUT DROP - -iptables -P FORWARD DROP - -iptables -P OUTPUT ACCEPT - -# 3. 필수 규칙 추가 - -# 로컬호스트 허용 - -iptables -A INPUT -i lo -j ACCEPT - -# 이미 연결된 세션 허용 - -iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - -# 4. 서비스별 규칙 추가 - -# SSH (22/tcp) 허용 - -iptables -A INPUT -p tcp --dport 22 -j ACCEPT - -# 웹서버 (80/tcp, 443/tcp) 허용 - -iptables -A INPUT -p tcp --dport 80 -j ACCEPT - -iptables -A INPUT -p tcp --dport 443 -j ACCEPT - -# Ping (ICMP) 허용 - -iptables -A INPUT -p icmp -j ACCEPT - -# 5. 설정된 규칙 확인 - -iptables -L -v -n - -# 실제 적용 규칙 - -iptables -F - -iptables -X - -iptables -t nat -F - -iptables -t nat -X - -iptables -t mangle -F - -iptables -t mangle -X - -iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # 연결된 세션 및 연관된 패킷 허용 # 연결된 세션 및 연관된 패킷 허용 *가장 먼저 적용* - -iptables -A INPUT -i lo -j ACCEPT #local host 접근 허용 - -iptables -A INPUT -p icmp -j ACCEPT - -iptables -A INPUT -s 192.168.0.6 -p tcp --dport 22 -j ACCEPT#콘솔 PC에서만 ssh 접근 허용 - -iptables -A INPUT -s 192.168.0.6 -p tcp --dport 8006 -j ACCEPT #콘솔 PC에서만 WEB UI 접근 허용 - -iptables -A INPUT -p tcp --dport 80 -j ACCEPT - -iptables -A INPUT -p tcp --dport 443 -j ACCEPT - -#*허용 규칙 이후 차단 규칙 적용* - -iptables -P INPUT DROP - -iptables -P FORWARD DROP - -iptables -P OUTPUT ACCEPT - -iptables -L -v -n - -sudo netfilter-persistent save 현재 설정 저장 - - - - - - - - - - - - PVE NIC 설정 - - vmbr - - - - PVE내에서 가상 L2 스위치 처럼 동작합니다. - - 일반적으로 vmbr에 할당된 IP 주소는 PVE의 host IP 입니다. - - vmbr은 L2 스위치이므로 IP주소를 할당하지 않아도 됩니다. (PVE Web UI에 접근하는 IP 주소 제외) - - VLAN을 지원합니다. - - STP(Spanning Tree Protocol; IEEE 802.1D)가 기본적으로 비활성화 되어 있습니다. - - - - vmbr 설정 - - 추후 OPNsense가 WAN/LAN 통신시 다른 물리 NIC로 통신하기 위하여 vmbr을 두 개 생성합니다. 이때, vmbr0은 이미 PVE의 웹 host용 vmbr로 생성되어 있습니다. 따라서 계획에 따라 vmbr1을 생성하고, vmbr1에 PVE의 Web용 host와 임시 Server를 연결합니다. - - - - 데이터센터 - pve - 시스템 - 네트워크 - vmbr0 - 수정 - - - - IPv4/CIDR 삭제 - - Gateway 삭제 - - - - - - 생성 - - - - 이름: vmbr1 - - IPv4/CIDR: vmbr0의 내용 - - Gateway: vmbr0의 내용 - - 자동시작 체크 - - 브릿지 포트: enp3s0 (NIC1) - - 생성 - - - - - - 설정 적용 - - - - STP - - STP는 Switch - Switch 간 브로드캐스트 발생 시 서로에게 무한하게 Broadcast 패킷을 보내는 루프로 인한 브로드캐스팅 스톰을 방지하기 위한 프로토콜입니다. 따라서, Switch가 STP를 지원하지 않으면 Switch - Switch간 이중화된 연결이 불가능합니다. 따라서, 한 vmbr 안에 여러 NIC를 할당하려면, STP를 활성화하여야, 네트워크가 정상 작동합니다. (권장되는 방식은 LACP를 사용하여, Bonding을 구성하는 것입니다.) - - #!/bin/bash - -#STP 활성화 방법 - -#shell 접속 - -cat /etc/network/interfaces - -sudo vi /etc/network/interfaces - -iface vmbr0 inet static - - address 192.168.0.48/24 - - gateway 192.168.0.1 - - bridge-ports enp2s0 # NIC 할당 - - bridge-stp on #STP 활성화(이 부분을 off에서 on으로 바꿔야 STP 활성화) - -# bridge-fd 0 #이 부분은 STP 활성화 시, 주석 처리하는 것이 권장 됩니다. - -#:wq! - -sudo systemctl restart networking #network.d 재실행 - - 다만, OPNsense에 두 NIC에 같은 LAN 대역의 다른 IP를 부여하여도, OPNsense 내에서 두 NIC를 각각 다른 End point 취급하므로(Switch 처럼 작동하지 않으므로) 정상 작동합니다. - - 결론 - - - - WTR Pro에 PVE 설치 완료 - - PVE 원격 접속 가능 - - admin 계정 생성 및 root 계정 설정 - - - - root 계정 ssh 접근 비활성화 - - root 계정 Web UI 접근 비활성화 - - pveadmin 계정 생성(2000:2000, sudo) - - Web UI 상 pveadmin 권한 부여 - - - - - -  vmbr 생성 및 설정 - - - - STP로 비활성화로 인한 주의점 명시 - - - - - - - - - - 2025-06-21 - 초안 작성 - - 2025-06-23 - vmbr 생성 및 설정, 주의점 추가 - - 2025-06-25 - iptables-persistent 설치 및 규칙 추가 - -4.3. PVE 상 VM/LXC 설치법 -목표 - - - - PVE 상 VM/LXC 설치법을 확인합니다. - - PVE 상 임시 Server (Debian)을 설치합니다. - - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - - - 요구 사항 - - - - 임시 Server(Debain)을 PVE에 설치 - - - - 수행 작업 - - VM/LXC 설치 전 사전 준비 - - VM과 LXC간 차이점 - - VM은 PVE와 분리된 별도의 OS입니다. PVE를 통해서 가상화된 하드웨어 위에 커널부터 격리된 OS를 설치합니다. 이와 반대로 LXC는 PVE와 커널을 공유하므로, 별도의 OS 설치 파일 없이 PVE 위에서 바로 생성이 가능합니다. - - 설치 파일 준비 (VM) - - Debian 설치 파일 준비 - - - - 공식 사이트: https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/ - - Debian-netins ISO 파일과 checksum 파일 다운로드 - - 파일을 다운 받은 후, Powershell에서  Get-FileHash -Path [file_path] -Algorithm SHA256 명령어를 통해 Hash 값을 확인 - - checksum 파일에 명시된 Hash값과 차이 없을 시 ISO 파일 관리용 USB에 Debian-netins 파일 복사(무결성 확인) - - - - 기타 설치 파일 준비 - - PVE는 Linux 뿐 아니라, Unix, Windows 등 다양한 OS를 지원하므로 VM 추가 필요시 해당 설치 파일을 구하여 설치할 수 있습니다. - - 설치 - - VM 설치 - - ISO 파일 업로드 - - - - 데이터센터 - PVE -  local(pve) - ISO 이미지 - 업로드 - - 파일 선택 - - - - OS 설치 파일 선택 - - 해시 알고리즘 선택 - - 해시 값 붙여 넣기 - - - - - - 업로드 - - - - TASK OK 확인 - - - - - - - - VM 설치 - - - - 데이터센터 - PVE 우클릭 - - VM 생성 클릭 - - 일반 설정 - - - - 노트: pve - - VM ID: 서버별 대표 UID와 동일하게 관리 - 임시 Server: 2999 - - 이름: temp-app - - 부팅시 시작 체크 - - 시작/종료 순서, 시작 지연, 종료 시간 제한: 추후 VM이 늘어날 때 설정 - - - - - - OS 설정 - - - - CD/DVD 디스크 이미지 파일(ISO) 사용 - - - - ISO 이미지 선택 - - - - - - 게스트 OS - - - - 유형: Linux (설치할 OS 종류에 따라) - - 버전: 6.x - 2.6 Kernel (설치할 OS 종류에 따라) - - - - - - - - - - 시스템 설정 - - - - 기본 설정 - - - - - - 디스크 설정 - - - - 스토리지: local-lvm 선택 - - 삭제(Discard): 체크 (파일이 삭제될 때 마다 SSD에 블록이 비었다고 알리는 형식, 실시간 TRIM) - - 디스크 크기: 59.60; 64GB (VM에 할당할 용량을 GiB 단위로 입력) - - - - - - PVE는 기본 용량 단위를 iB 체계를 사용하므로, B체계 사용 시 변환 작업 필요 - - - - 추후 VM - 하드웨어 - 디스크 Action 에서 디스크 작업 크기조정으로 용량 변경 가능, VM OS 내 파티션 변경 작업 별도 - - - - - - 버스/디바이스: VirtlO Block (기본 값, 가장 좋은 성능)/VM 내부 디스크의 순서 - - 디스크 형식: Raw Disk Image (약간의 성능상 우위, 고급기능 X) - - - - - -  CPU 설정 - - - - 소켓: 1 (pCPU 갯수, 1개)  - - Cores: 2 (원하는 vCPU 갯수 만큼) - - vCPUs: 2 (Socket x Cores) - - CPU 제한: (추후 VM 여러 개 생성시 지정) - - - - - - Memory 설정 - - - - Memory: 5722(약 6GB) (계획에 따라. MiB 단위로 입력) - - - - - - 네트워크 설정 - - - - 브릿지: vmbr1 (계획에 따라) - - 모델: VirtlO - - VLAN 태그: 없음 (계획에 따라) - - 방화벽 체크 해제 (추후 iptable등 UFW 사용) - - 추후 VM - 하드웨어 탭에서 네트워크 인터페이스 추가 가능 - - - - - - 확인 - - - - 생성 후 시작 체크 해제 - - - - - - VM - 하드웨어 - 네트워크 디바이스 에서 생성된 MAC 주소 확인하여 DHCP 예약 추가 - - VM - 옵션 - 부팅 순서 변경 - - - - virtio(HDD/SSD) - - ide(CD/DVD) - - net(network)  - - - - 해당 설정 미실행시, 부팅 불가 현상 발생 가능 - - - - - - - - - - LXC 설치 - - LXC 템플릿 다운로드 - - - - 데이터센터 - PVE -  local(pve) - LXC 템플릿 - 템플릿 - - 패키지 선택 - - - - debian 선택 - - - - - - 다운로드 - - - - TASK OK 확인 - - - - - - - - LXC 설치(VM과 다른 부분만) - - - - 데이터센터 - PVE 우클릭 - - CT 생성 클릭 - - 일반 설정 - - - - CT ID: 서버별 대표 UID와 동일하게 관리 - - 권한 없는 컨테이너 체크 (unprivileged LXC) - - 부팅시 시작 체크 - - 시작/종료 순서, 시작 지연, 종료 시간 제한: 추후 VM이 늘어날 때 설정 - - 비밀번호 입력 - - SSH 공개키 입력 (선택 사항) - - - - - - 템플릿 - - - - 템플릿 선택 - - - - - - 디스크 설정 - - - - 마운트 옵션: Discard - - - - - - CPU - - - - Cores만 선택 - - - - - - 네트워크 - - - - 이름: eth0 - - 브릿지: vmbr1 - - IPv4: Static/DHCP 선택 - - - - - - DNS - - 확인 - - - - 생성 후 시작 체크 해제 - - - - - - LXC - 하드웨어 - 네트워크 디바이스 에서 생성된 MAC 주소 확인하여 DHCP 예약 추가 - - - - 결론 - - - - 임시 Server VM 설치 준비 완료 - - - - - - 2025-06-24 - 초안 작성 완료 - -5. 임시 Server 설정 - -5.x. NPM 설정 -목표 - - 임시 Server에 NPM을 설치합니다. - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - 요구 사항 - - NPM 설치 - NPM에 내부망 접속 - NAT 설정 - SSL/TLS 이용을 위한 인증서를 적용 - - 수행 작업 - docker-compose-npm.yml 작성 - 도커 설정 - - 이미지 - - jc21/nginx-proxy-manager:latest - - -  포트 - - 81:81 - 80:80 - 443:443 - - - 볼륨 마운트 - - - /home/temp-app/docker/nginx-proxy-manager/data:/data - - /home/temp-app/docker/nginx-proxy-manager/ - - - - 환경 변수 - - 없음 - - - DB 연동시 다음과 같은 환경변수 설정 필요(MYSQL 부분은, POSTGRES 사용 시 POSTGRES로 대체 가능) - - DB_MYSQL_HOST - DB_MYSQL_PORT - DB_MYSQL_USER - DB_MYSQL_PASSWORD - DB_MYSQL_NAME - - - 하지만 현재 임시 서버 환경이므로 간단한 SQLite를 사용(환경 변수 필요 없음) - - docker-compose.yml 작성 - - 도커 컨테이너를 위한 디렉터리 생성 - - mkdir -p /home/temp-app/docker/nginx-proxy-manager/{data,letsencrypt,websites} - - - 디렉토리 권한 변경 - - chmod -R 755 /home/temp-app/docker/ngnix-proxy-manager - chmod -R 700 /home/temp-app/docker/ngnix-proxy-manager/letsencrypt - - - /home/temp-app/docker/ngnix-proxy-manager 위에서 docker-compose-npm.yml 작성 - - docker-compose.yml은 'tab'을 인식하지 않으므로 'space'를 사용  - - services: -app: -image: 'jc21/nginx-proxy-manager:latest' -restart: unless-stopped -ports: -# These ports are in format : -- '80:80' # Public HTTP Port -- '443:443' # Public HTTPS Port -- '81:81' # Admin Web Port -# environment: -# Uncomment this if you want to change the location of -# the SQLite DB file within the container -# Mysql/Maria connection parameters: -#DB_MYSQL_HOST: "db" -#DB_MYSQL_PORT: 3306 -#DB_MYSQL_USER: "npm" -#DB_MYSQL_PASSWORD: "npm" -#DB_MYSQL_NAME: "npm" -# Postgres parameters: -#DB_POSTGRES_HOST: 'db' -#DB_POSTGRES_PORT: '5432' -#DB_POSTGRES_USER: 'npm' -#DB_POSTGRES_PASSWORD: 'npmpass' -#DB_POSTGRES_NAME: 'npm' -volumes: -- ./data:/data -- ./letsencrypt:/etc/letsencrypt -#the direcrtory for static web files which you want to host -- ./websites:/mnt/user/appdata/NginxProxyManager/websites - - - - - 도커 실행 - - docker-compose.yml 파일이 있는 곳에서 docker compose -f docker-compose-npm.yml up -d 명령어 실행 - - iptables 설정 변경 - - sudo iptables -A INPUT -s 192.168.0.6 -p tcp --dport 81 -j ACCEPT # 콘솔 PC에서만 웹 UI 접근 허용 - sudo iptables -L -v -n # 설정 확인 - sudo netfilter-persistent save # 설정 적용 - - 기존에 iptables 설정에서 이미 -p tcp --dport 443, -p tcp --dport 80은 이미 허용 - - - 웹 UI 접속 - 초기 설정 - - [temp-app_ip]:81로 접속 - 초기 계정: admin@example.com/changeme - Fullname, nickname, password 입력하여 변경 - 비밀번호 변경 - - SSL/TLS 인증서 발급 - - 인증서 발급 전 T5004의 포트 포워딩 규칙 추가 - - 443포트를 temp-app 서버로 포트 포워딩 - - - 메뉴 - SSL certificates - add SSL certificate - - Domain names: example.com - email adress for Let's Encrypt - Use DNS Challenge - - DNS providor: cloudflare - Credentials File Content: API 키 입력(DDNS 사용 시 사용했던 것) - - - - - 메뉴 - proxy hosts - add new proxy host  - - 결과 - -5.4. DB 설치 -목표 - - 임시 Server에 DBMS를 설치합니다. - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - 요구 사항 - - MariaDB 설치 - PostgreSQL 설치 - DBMS 계정 설정 - Redis 설치 - - 수행 작업 - 결과 - -5.1. Debian 설치 -목표 - - - - PVE 상 Debian을 설치합니다. - - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - - - 요구 사항 - - - - Debian VM을 PVE 위에 설치 - - Debian의 초기 설정 - - - - 수행 작업 - - Debian 설치 - - VM 실행 - - - - 데이터센터 - pve - 2999(temp-app) - 콘솔 - - Start now - - - - Debian 설치 - - - - Debian 설치 화면  Graphic Install 클릭 - - 언어 및 위치 설정 - - - - 언어: korean - - 위치: 대한민국 - - 키보드 설정: 한국어 - - - - - - 네트워크 설정 - - - - 호스트 이름: temp-app - - 도메인 네임: temp-app.example.com - - - - - - 사용자 계정 설정 - - - - root 암호 설정 - - 사용자 이름: temp-app - - 사용자 암호 설정 - - - - - - 디스크 설정 - - - - 디스크 파티션 설정: 자동 - 디스크 전체 사용 - - 가상 디스크 선택 - - 파티션: 모두 한 파티션에 사용 (추후 다른 서버 구축할 때 NFS 적용시, 분리 필요) - - 파티션 나누기를 마치고 바뀐 사항을 디스크에 쓰기 - - 바뀐 점을 디스크에 쓰시겠습니까?: 예 - - - - - - 패키지 관리자 - - - - 추가 설치 미디어를 검사하겠습니까?: 아니요 - - 지역: 대한민국 - - 아카이브 미러 사이트: deb.debian.org - - http 프록시 설정: 없음 - - 패키지 인기 투표: 안함 - - - - - - 소프트웨어 선택 - - - - SSH Server 체크 - - 표준 시스템 유틸리티 체크 - - 나머지 전부 체크 해제 - - - - - - GRUB 부트로더 - - - - 사용 - - 장치: 메인 저장소(/dev/vda 등) - - - - - - ISO 파일 제거 - - - - 데이터센터 - pve -  2999(temp-app)  -  하드웨어 - CD/DVD 드라이브 - 수정 - - 미디어 사용 안함 - - - - - - VM 재시작 - - - - 데이터센터 - pve - 2999(temp-app) - 콘솔 - - 계속 - - - - - - - - Debian 초기 설정 - - Debian 로그인 - - - - root/password - - - - 필수 패키지 설치 - - #!/bin/bash - -apt update - -apt install sudo curl iptables-persistent #sudo와 curl, iptables-persistent 기본 설치 안되어 있음 - -apt update - - 계정 정보 확인 및 수정 - - #!/bin/bash - -#계정 정보 확인 - -cat /etc/passwd - -#temp-app 1000:1000 처음 설치시 설정한 계정 temp-app이 1000:1000이므로 수정 - -groupadd -g 2000 svadmins #svadmins 그룹 생성 - -usermod -u 2999 -g 2000 -s /bin/bash -c "Temp-app Admin" -G sudo temp-app - -#-u [UID] -g [GID] -s [shell] -c [discription] -G [additional group] user_name - -#User name을 변경하고 싶을 시 -l [user_name]을 입력한다. - -id temp-app - -#결과 예시: uid=2999(temp-app) gid=2000(pveadmins) groups=2000(pveadmins),27(sudo) - - root 계정 ssh 로그인 금지 - - - - sshd_config 확인 - - - - - - #!/bin/bash - -cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak - -cat /etc/ssh/sshd_config - -#PermitRootLogin 옵션 확인 후 명시적인 차단 - -vi /etc/ssh/sshd_config - -#PermitRootLogin no - -sudo service sshd restart - - - - - - - - ssh 접속하여 root 로그인 가능 여부 확인 - - - - ssh [id]@[ip] - - 접속 불가 확인(permission denied) - - - - - - ssh 접속하여 일반 계정 로그인 가능 여부 확인 - - - - ssh [id]@[ip] - - 접속 가능 확인 - - - - - - - - iptables 설정 - - #!/bin/bash - -sudo apt install iptables-persistent #debian iptables 저장 패키지 - -#Save current IPv4 Rulse 선택 - -sudo apt update - -# 명령어 설명 - -# 1. 모든 규칙 초기화 - -iptables -F - -iptables -X - -iptables -t nat -F - -iptables -t nat -X - -iptables -t mangle -F - -iptables -t mangle -X - -# 2. 기본 정책 설정 (Default-Drop) - -iptables -P INPUT DROP - -iptables -P FORWARD DROP - -iptables -P OUTPUT ACCEPT - -# 3. 필수 규칙 추가 - -# 로컬호스트 허용 - -iptables -A INPUT -i lo -j ACCEPT - -# 이미 연결된 세션 허용 - -iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - -# 4. 서비스별 규칙 추가 - -# SSH (22/tcp) 허용 - -iptables -A INPUT -p tcp --dport 22 -j ACCEPT - -# 웹서버 (80/tcp, 443/tcp) 허용 - -iptables -A INPUT -p tcp --dport 80 -j ACCEPT - -iptables -A INPUT -p tcp --dport 443 -j ACCEPT - -# Ping (ICMP) 허용 - -iptables -A INPUT -p icmp -j ACCEPT - -# 5. 설정된 규칙 확인 - -iptables -L -v -n - -# 실제 적용 규칙 - -iptables -F - -iptables -X - -iptables -t nat -F - -iptables -t nat -X - -iptables -t mangle -F - -iptables -t mangle -X - -iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # 연결된 세션 및 연관된 패킷 허용 # 연결된 세션 및 연관된 패킷 허용 *가장 먼저 적용* - -iptables -A INPUT -i lo -j ACCEPT #local host 접근 허용 - -iptables -A INPUT -p icmp -j ACCEPT - -iptables -A INPUT -s 192.168.0.6 -p tcp --dport 22 -j ACCEPT#콘솔 PC에서만 ssh 접근 허용 - -iptables -A INPUT -p tcp --dport 80 -j ACCEPT - -iptables -A INPUT -p tcp --dport 443 -j ACCEPT - -#*허용 규칙 이후 차단 규칙 적용* - -iptables -P INPUT DROP - -iptables -P FORWARD DROP - -iptables -P OUTPUT ACCEPT - -iptables -L -v -n - -sudo netfilter-persistent save 현재 설정 저장 - - 결론 - - - - PVE 상 Debian 설치 완료 - - root 계정 및 local 계정 설정 - - - - ssh root 계정 접근 차단 - - - - - - 필수 패키지(iptables, sudo, curl) 설치 완료 - - iptables 규칙 적용 - - - - - - 2025-06-25 - 초안 작성 - -5.2. DDNS 설정 -목표 - - - - 임시 Server에서 Cloudflare API를 통한 DDNS를 설정합니다. - - 설정 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - - - 요구 사항 - - - - Cloudflare API를 통한 DDNS 설정 shell script 프로그램 작성 - - - - 도메인 등록, 갱신, 삭제 기능 구현 - - - - - - cron을 통한 shell script 자동 실행 - - - - 수행 작업 - - Cloudflare Domain 구입 - - - - https://www.cloudflare.com 접속 - - 로그인 후 대시보드 로 이동 - - 도메인 등록 - 도메인 등록 으로 이동하여 도메인 구입 - - 도메인 등록 - 도메인 관리 로 이동하여 구매 도메인 활성 확인 - - - - DDNS 스크립트 작성 - - API 키 확인 - - - - https://www.cloudflare.com 접속 - - 내 프로필 - API 토큰 으로 이동 - - 토큰 생성 - 영역 DNS 편집 - 템플릿 사용 - - 권한 - - - - 영역 - DNS - 편집 - - - - - - 영역 리소스 - - - - 포함 - 특정 영역 - Domain - - 포함 - - - - - - - 요약 계속 - 토큰 생성 - - 이 토큰 테스트에 있는 명령어 실행하여 정상 작동하는 지 확인 후 복사 - - - - - - #!/bin/bash - - curl "https://api.cloudflare.com/client/v4/user/tokens/verify" \ - - - --H "Authorization: Bearer $API_KEY" - - #결과 예시: {"result":{"id":"---","status":"active"},"success":true,"errors":[],"messages":[{"code":10000,"message":"This API Token is valid and active","type":null}]} - - - - - - - - 계정 홈 - 사용할 도메인 클릭 - - - - API - 영역 ID(Zone_ID) 확인 후 복사 - - - - - - - - Cloudflare API 스크립트 확인 - - - - https://developers.cloudflare.com/api 접속 - - Ctrl + k 눌러 DNS Record 검색 후 이동 - - API 스크립트 확인 - - - - - - #!/bin/bash - -#List DNS Records - -curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$DOMAIN" \ - --H "Authorization: Bearer $API_KEY" - -#결과가 없을 시 "result": [], success: "true", ... - -#결과 형식 "result": [{..., ...}, {..., ...},...], success: "true", ... - -#Create DNS Record - -curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \ - - -H 'Content-Type: application/json' \ - --H "Authorization: Bearer $API_KEY" \ - - -d "{ - - \"name\": \"$DOMAIN\", - - \"ttl\": 3600, - - \"type\": \"A\", - - \"comment\": \"Domain verification record\", - - \"content\": \"$IP\", - - \"proxied\": true - - }" - -#Overwrite DNS Record - -curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID" \ - - -X PUT \ - - -H 'Content-Type: application/json' \ - --H "Authorization: Bearer $API_KEY" \ - - -d "{ - - \"name\": \"$DOMAIN\", - - \"ttl\": 3600, - - \"type\": \"A\", - - \"comment\": \"Domain verification record\", - - \"content\": \"$IP\", - - \"proxied\": true - - }" - -#Delete DNS Record - -curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNS_RECORD_ID" \ - - -X DELETE \ - - -H 'Content-Type: application/json' \ - --H "Authorization: Bearer $API_KEY" \ - - }" - - - - - - - - - - Shell script 작성 - - - - Script 파일을 생성합니다. - - - - touch /home/temp-app/ddns/ddns.sh - - touch /home/temp-app/ddns/ddns.conf - - - - - - Script 파일 실행 권한을 부여합니다. - - - - chmod 744 /home/temp-app/ddns/ddns.sh - - - - - - 설정 파일에 소유자 읽기 권한만 부여합니다. - - - - chmod 600 /home/temp-app/ddns/ddns.conf - - - - - - Script 파일을 작성합니다. - - - - getopts 를 활용하여 Domain, proxied, ttl, 삭제 여부 등을 매개변수로 받습니다. - - 함수를 활용하여, Cloudflare DNS API기능을 정의합니다. (조회, 생성, 업데이트, 삭제) - - 최대한 범용성 있는 스크립트를 만듭니다. - - vi /home/temp-app/ddns/ddns.sh - - - - #!/bin/bash - -DOMAIN="" - -TTL=180 # basic value - -C_TTL=86400 - -PROXIED="false" # basic value - -DELETE_FLAG=0 - -CURRENT_IP="" - -FLAG="" - -# usage func - -usage() { - - echo "Usage: $0 -d \"domain\" [-t \"ttl\"] [-p] [-r] [-c]" - - echo " -d : Specify the domain to update." - - echo " -t : Specify the TTL (Time to Live)." - - echo " -p : Specify the cloudflare proxy to use." - - echo " -r : Delete the DNS record." - - exit 1 - -} - -# using getopts to get arguemnts - -while getopts "d:t:pr" opt; do - - case $opt in - - d) - - DOMAIN="$OPTARG" - - ;; - - t) - - TTL="$OPTARG" - - ;; - - p) - - PROXIED="true" - - ;; - - r) - - DELETE_FLAG=1 - - ;; - - \?) # unknown options - - echo "Invalid option: -$OPTARG" >&2 - - usage - - ;; - - :) # paramenter required option - - echo "Option -$OPTARG requires an argument." >&2 - - usage - - ;; - - esac - -done - -# get option and move to parameters - -shift $((OPTIND - 1)) - -# check necessary option - -if [ -z "$DOMAIN" ]; then - - echo "Error: -d option (domain) is required." >&2 - - usage - -fi - -if ! [[ "$TTL" =~ ^[0-9]+$ ]] || [ "$TTL" -le 0 ]; then - - echo "Error: -t option (ttl) requires a number above 0." >&2 - - usage - -fi - -# change directory for cron - -cd "$(dirname "$0")" - -# make config directory and log directory - -if [ ! -d "./config" ]; then - - mkdir ./config - -fi - -if [ ! -d "./log" ]; then - - mkdir ./log - -fi - -LOG_FILE="./log/ddns_$(date "+%Y-%m-%d").log" - -CONF_FILE="./config/ddns.conf" - -# log func - -log() - -{ - - local text="$1" - - echo "---------" >> "$LOG_FILE" - - echo -e "$(date "+%Y-%m-%d %H:%M:%S"): $text" >> $LOG_FILE - -} - -# check and create log file - -if [ ! -f "$LOG_FILE" ]; then - - log "Notice: log file is created" - -fi - -# check and create conf file - -if [ ! -f "$CONF_FILE" ]; then - - log "Error: Set ./config/ddns.conf first" - - echo -e "#!/bin/bash" >> $CONF_FILE - - echo -e "# --- ddns.conf ---" >> $CONF_FILE - - echo -e "ZONE_ID=\"\"" >> $CONF_FILE - - echo -e "API_KEY=\"\"" >> $CONF_FILE - - chmod 600 "$CONF_FILE" - - exit - -fi - -# check environmental value - -source "$CONF_FILE" - -if [ -z "$ZONE_ID" -o -z "$API_KEY" -o -z "$DOMAIN" ]; then - - log "Error: There is no correct option in \"ddns.conf\"\nZONE_ID, API_KEY are needed" - - exit - -fi - -# check package - -if ! command -v curl &> /dev/null; then - - log "Error: curl package is needed" - - exit - -fi - -if ! command -v jq &> /dev/null; then - - log "Error: jq package is needed" - - exit - -fi - -# API options - -URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" - -CONTENT_TYPE="Content-Type: application/json" - -AUTHORIZATION="Authorization: Bearer $API_KEY" - -CURRENT_IP=$(curl -sf "https://ifconfig.me") ||\ - -CURRENT_IP=$(curl -sf "https://ifconfig.kr") ||\ - -CURRENT_IP=$(curl -sf "https://api.ipify.org") - -if [ "$CURRENT_IP" == "" ]; then - - log "Error: Can't get an IP" - - exit - -fi - -get_dns_record() - -{ - - local type="$1" - - local name="$2" - - local response="$( - - curl -s "$URL?type=$type&name=$name"\ - - -H "$CONTENT_TYPE"\ - - -H "$AUTHORIZATION")" - - if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then - - log "Error: Can't get dns record\"Reason: $response" - - exit - - else - - echo "$response" - - fi - -} - -create_dns_record() - -{ - - local type="$1" - - local name="$2" - - local ttl="$3" - - local comment="$4" - - local content="$5" - - local response="$( - - curl -s "$URL"\ - - -X POST\ - - -H "$CONTENT_TYPE"\ - - -H "$AUTHORIZATION"\ - - -d "{ - - \"name\": \"$name\", - - \"ttl\": $ttl, - - \"type\": \"$type\", - - \"comment\": \"$comment\", - - \"content\": \"$content\", - - \"proxied\": $PROXIED - - }")" - - if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then - - log "Error: Can't create dns record\"Reason: $response" - - exit - - else - - echo "$response" - - fi - -} - -update_dns_record() - -{ - - local type="$1" - - local name="$2" - - local ttl="$3" - - local comment="$4" - - local content="$5" - - local id="$6" - - local response=$( - - curl -s "$URL/$id"\ - - -X PUT\ - - -H "$CONTENT_TYPE"\ - - -H "$AUTHORIZATION"\ - - -d "{ - - \"name\": \"$name\", - - \"ttl\": $ttl, - - \"type\": \"$type\", - - \"comment\": \"$comment\", - - \"content\": \"$content\", - - \"proxied\": $PROXIED - - }") - - if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then - - log "Error: Can't update dns record\"Reason: $response" - - exit - - else - - echo "$response" - - fi - -} - -delete_dns_record() - -{ - - local type="$1" - - local id="$2" - - local response=$( - - curl -s "$URL/$id"\ - - -X DELETE\ - - -H "$CONTENT_TYPE"\ - - -H "$AUTHORIZATION" - - ) - - if [ "$(echo "$response" | jq -r '.success')" == "false" ]; then - - log "Error: Can't delete dns record\"Reason: $response" - - exit - - else - - echo "$response" - - fi - -} - -A_DNS_RECORD=$(get_dns_record "A" "$DOMAIN") - -S_DNS_RECORD=$(get_dns_record "cname" "*.$DOMAIN") - -W_DNS_RECORD=$(get_dns_record "cname" "www.$DOMAIN") - -if [ "$DELETE_FLAG" -eq 1 ]; then # Delete flag - - FLAG="false" - - if [ "$(echo $A_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then - - A_DNS_ID="$(echo $A_DNS_RECORD | jq -r '.result[0].id')" - - delete_dns_record "A" "$A_DNS_ID" - - log "Delete: root DNS record is deleted" - - FLAG="true" - - fi - - if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then - - S_DNS_ID="$(echo $S_DNS_RECORD | jq -r '.result[0].id')" - - delete_dns_record "cname" "$S_DNS_ID" - - log "Delete: sub DNS record is deleted" - - FLAG="true" - - fi - - if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then - - W_DNS_ID="$(echo $W_DNS_RECORD | jq -r '.result[0].id')" - - delete_dns_record "cname" "$W_DNS_ID" - - log "Delete: www DNS record is deleted" - - FLAG="true" - - fi - - if [ "$FLAG" == "false" ]; then - - log "Notice: Nothing is Deleted. There are no DNS records" - - fi - - exit - -fi - -if [ "$(echo $A_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # root DNS record exist - - A_DNS_ID="$(echo $A_DNS_RECORD | jq -r '.result[0].id')" - - A_DNS_CONTENT="$(echo $A_DNS_RECORD | jq -r '.result[0].content')" - - A_DNS_TTL="$(echo $A_DNS_RECORD | jq -r '.result[0].ttl')" - - A_DNS_PROXIED="$(echo $A_DNS_RECORD | jq -r '.result[0].proxied')" - - if [ "$A_DNS_CONTENT" != $CURRENT_IP -o "$A_DNS_TTL" != "$TTL" -o "$A_DNS_PROXIED" != "$PROXIED" ]; then - - update_dns_record "A" "$DOMAIN" "$TTL" "$(date "+%Y-%m-%d %H:%M:%S"): root domain from ddns.sh" "$CURRENT_IP" "$A_DNS_ID" - - log "Update: Root DNS record is successfully changed\nDomain: $DOMAIN\nIP: $A_DNS_CONTENT to $CURRENT_IP\nTTL: $A_DNS_TTL to $TTL\nproxied: $A_DNS_PROXIED to $PROXIED" - - else - - log "Notice: Root DNS record is not changed\nDomain: $DOMAIN\nIP: $CURRENT_IP\nTTL: $TTL\nproxied: $PROXIED" - - fi - -else # root DNS record does not exist - - create_dns_record "A" "$DOMAIN" "$TTL" "$(date "+%Y-%m-%d %H:%M:%S"): root domain from ddns.sh" "$CURRENT_IP" - - log "Create: Root DNS record is successfully created\nDomain: $DOMAIN\nIP: $CURRENT_IP\nTTL: $TTL\nproxied: $PROXIED" - -fi - -if [ "$(echo $S_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # sub DNS record exist - - S_DNS_ID="$(echo $S_DNS_RECORD | jq -r '.result[0].id')" - - S_DNS_CONTENT="$(echo $S_DNS_RECORD | jq -r '.result[0].content')" - - S_DNS_TTL="$(echo $S_DNS_RECORD | jq -r '.result[0].ttl')" - - S_DNS_PROXIED="$(echo $S_DNS_RECORD | jq -r '.result[0].proxied')" - - if [ "$S_DNS_CONTENT" != "$DOMAIN" -o "$S_DNS_TTL" != "$C_TTL" -o "$S_DNS_PROXIED" != "$PROXIED" ]; then - - update_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" "$S_DNS_ID" - - log "Update: Sub DNS record is successfully changed\nDomain: $S_DNS_CONTENT to *.$DOMAIN\ncname: $DOMAIN \nTTL: $S_DNS_TTL to $C_TTL\nproxied: $S_DNS_PROXIED to $PROXIED" - - else - - log "Notice: Sub DNS record is not changed\nDomain: *.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED" - - fi - -else # sub DNS record does not exist - - create_dns_record "cname" "*.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): sub domain from ddns.sh" "$DOMAIN" - - log "Create: Sub DNS record is successfully created\nDomain: *.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED" - -fi - -if [ "$(echo $W_DNS_RECORD | jq -r '.result | length')" -eq 1 ]; then # www DNS record exist - - W_DNS_ID="$(echo $W_DNS_RECORD | jq -r '.result[0].id')" - - W_DNS_CONTENT="$(echo $W_DNS_RECORD | jq -r '.result[0].content')" - - W_DNS_TTL="$(echo $W_DNS_RECORD | jq -r '.result[0].ttl')" - - W_DNS_PROXIED="$(echo $W_DNS_RECORD | jq -r '.result[0].proxied')" - - if [ "$W_DNS_CONTENT" != "$DOMAIN" -o "$W_DNS_TTL" != "$C_TTL" -o "$W_DNS_PROXIED" != "$PROXIED" ]; then - - update_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" "$W_DNS_ID" - - log "Update: www DNS record is successfully changed\nDomain: $W_DNS_CONTENT to www.$DOMAIN\ncname: $DOMAIN\nTTL: $W_DNS_TTL to $C_TTL\nproxied: $W_DNS_PROXIED to $PROXIED" - - else - - log "Notice: www DNS record is not changed\nDomain: www.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED" - - fi - -else # www DNS record does not exist - - create_dns_record "cname" "www.$DOMAIN" "$C_TTL" "$(date "+%Y-%m-%d %H:%M:%S"): www domain from ddns.sh" "$DOMAIN" - - log "Create: www DNS record is successfully created\nDomain: www.$DOMAIN\ncname: $DOMAIN\nTTL: $C_TTL\nproxied: $PROXIED" - -fi - - - - vi /home/temp-app/ddns/config/ddns.conf   - - - - #!/bin/bash - -#ddns.conf - -# --- Cloudflare DDNS update script --- - -# --- ddns.conf --- - -#"Your Cloudflare Zone ID" - -ZONE_ID="" - -#"Your Cloudflare API key" - -API_KEY="" - - - - - - - - - - Shell script 실행 환경 만들기 - - 해당 쉘 스크립트는 jq라는 jason 파일 처리 명령어가 필요합니다. 따라서 스크립트를 실행하기 앞서 jq 명령어를 설치합니다. - - #!/bin/bash - -sudo apt update - -sudo apt install jq -y - -sudo apt update - - - - Shell script 실행 및 cron 등록하기 - - - - 스크립트를 실행합니다. - - - - /home/temp-app/ddns/ddns.sh -d example.com - - - - - - ddns.log 파일을 확인합니다. - - - - cat /home/temp-app/ddns/log/[date]_ddns.log - - - - --------- - -2025-06-27 02:49:02: Notice: log file is created - ---------- - -2025-06-27 02:49:04: Create: Root DNS record is successfully created - -Domain: example.com - -IP: 0.0.0.0 - -TTL: 300 - -proxied: true - ---------- - -2025-06-27 02:49:04: Create: Sub DNS record is successfully created - -Domain: *.example.com - -cname: example.com - -TTL: 400 - -proxied: true - ---------- - -2025-06-27 02:49:05: Create: www DNS record is successfully created - -Domain: www.example.com - -cname: example.com - -TTL: 400 - -proxied: true - ---------- - - - - - - - -  등록된 DNS가 작동하는 지 확인합니다. - - - - ping example.com - - - - - - 정상 실행이 확인되면 cron을 등록합니다. - - - - - - #!/bin/bash - -crontab -e # crontab 파일 편집 - -*/5 * * * * /home/temp-app/ddns/ddns.sh # 분 시 일 월 요일 [실행 파일], 매 5분마다 실행 - -#(*,-/) 사용하여 기간 조정 가능 - -#*/[숫자]: 매 [숫자] 분/시/일 ... 마다 반복 - -#[숫자], [숫자]: [숫자], [숫자] 분/시/일 마다 반복 - -#[숫자]-[숫자]: [숫자] 분/시/일 부터 [숫자] 분/시/일까지 매 분/시/일 반복 등등 - -crontab -l # crontab 파일 확인 - -#crontab -r # crontab 삭제 - -#crontab 시작 - -service cron start - -#crontab 중지 - -#service cron stop - -#crontab 작동 확인 - -#service cron status - -#crontab 재시작 - -#service cron restart - - - - - - - - 몇 분 후 ddns.log 파일 확인하여 정상 작동하는 지 확인합니다. - - - - cat /home/temp-app/ddns/log/ddns.log - - sudo service cron status - - - - - - - - 결과 - - - - jq 패키지 설치 - - DDNS 스크립트 작성 - - - - API를 통한 등록, 갱신, 삭제 기능 구현 - - JSON 형식에 대한 이해 - - jq를 통한 JSON 파일 조작에 애한 이해 - - - - - - DNS 및 DDNS 이해 - - A, AAAA, CNAME, TTL, PROXIED 등의 레코드의 의미 이해 - - cron 등록 - - - - - - 2025-06-25 - 초안 작성 - -5.3. Docker 설치 환경 설정 -목표 - - - - 임시 Server에 Docker 설치 환경을 설정합니다. - - 설치 절차 및 방법을 순서에 따라 명확하게 문서로 기록합니다. - - - - 요구 사항 - - - - 임시 서버에 Docker 설치 환경 설정 - - 관리자 계정 Docker 그룹 추가 - - - - 수행 작업 - - Docker 설치 환경 설정 - -  Docker docs 확인 - - - - Docker docs 접속 - - - - Debian 상 engine 설치를 위하여 다음 페이지 접속: https://docs.docker.com/engine/install/debian/ - - - - - - Docker docs의 절차를 따라 Docker engine 설치 - - - - 임시 Server 접속 - - 임시 Server가 구성되었으므로 더 이상 PVE web UI를 통한 접속이 아닌 SSH로 접속합니다. - - - - ssh temp-app@[ip_address] - - pw 입력 - - - - 이전 버전 제거 - - - - 운영 체제에서 자체적으로 지원하는  비공식적 인 도커 이미지를 확인합니다. - - - - docker.io - - docker-compose - - docker-doc - - podman-docker - - (docker engine은 containerd와 runc에 의존합니다. 만약 설치되어 있다면 추가로 제거하면 됩니다.) - - - - - - 다음 명령어를 통해 패키지 제거를 시도합니다. - - - - for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done - - 설치된 패키지가 없는 경우  패키지가 설치되지 않았습니다. 라는 반환이 나올 수 있습니다. - - 클린 설치를 원할 시 uninstall docker engine 섹션을 다시 확인합니다. - - - - - - - - 설치 과정 - - 도커는 다음과 같은 경로로 설치될 수 있습니다. - - - - Linux Desktop 환경을 위한 bundle - - apt 명령어를 통한 설치 - - manual 설치(업그레이드 포함) - - script를 통한 설치(오직 테스트 및 개발 환경에서만 추천합니다.) - - - - 현재 임시 Server는 CLI 환경이므로 apt 명령어를 통해 설치합니다. - - Docker Engine 설치 - - Docker Engine은 Docker가 구동될 수 있도록 만들어 주는 핵심 부분입니다. Docker의 CLI 명령어를 통해, 컨테이너를 만들고, 실행하고, 관리하는 모든 작업을 수행합니다. - - - - 설치의 앞서 Docker의 apt 저장소를 설정합니다. - - - - - - #!/bin/bash - -# Add Docker's official GPG key: - -sudo apt-get update - -sudo apt-get install ca-certificates curl - -sudo install -m 0755 -d /etc/apt/keyrings - -sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc - -sudo chmod a+r /etc/apt/keyrings/docker.asc - -# Add the repository to Apt sources: - -echo \ - - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ - - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - -sudo apt-get update - - - - 만약 kali같은 특수한 Linux 파생 버전을 사용한다면,  $(. /etc/os-release && echo "VERSOION_CODENAME") 부분을  brookworm 등으로 대체해야 할 수 있습니다. - - - - - -  다음 명령어를 통해 Docker의 최신 버전을 설치합니다. - - - - - - #!/bin/bash - -sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - - 만약 특정 버전을 설치하고 싶다면 다음 명령어를 통해 설치합니다. - - - - #!/bin/bash - -#List the available versions: - -apt-cache madison docker-ce | awk '{ print $3 }' - -5:28.3.0-1~debian.12~bookworm - -5:28.2.2-1~debian.12~bookworm - -.... - -#Select the version which you want - -VERSION_STRING=5:28.3.0-1~debian.12~bookworm - -#Install - -sudo apt-get install docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-buildx-plugin docker-compose-plugin - - - - - - - - hello-world 이미지를 실행하여 성공적으로 설치가 되었는지 확인합니다. - - - - sudo docker run hello-world - - Hello from Docker! 라는 멘트를 확인합니다. - - - - - - hello-world 이미지를 제거합니다. - - - - docker container ls -a - - hello-world 컨테이너 명 확인 - - docker rm [컨테이너 명] - - docker images ls - - hello-world 이미지 id 확인 - - docker rmi [이미지 id] - - - - - - 업데이트 시 해당 절차의 2. 로 돌아가 명령어를 다시 실행합니다. - - - - Docker Compose 설치 - - Docker Compose는 여러 개의 컨테이너로 구성된 애플리케이션을 한번의 정의하고 관리하거나, 복잡한 설정을 파일에서 관리할 수 있도록 하는 도구입니다. docker run의 명령어들을 모아 yaml 파일인 docker-compose.yml을 통해 정의하고 제어할 수 있도록 도와줍니다. - - - - Docker engine이 설치를 완료 한 뒤 이어서 Docker compose를 설치합니다. - - - - - -   - - #!/bin/bash - -#Install docker compose plugin - -sudo apt-get update - -sudo apt-get install docker-compose-plugin - -#Check docker compose version - -sudo docker compose version - - - - - - - - 서버 관리 계정에 docker 그룹을 추가하여, sudo 없이 docker 명령어를 사용할 수 있도록 합니다. - - - - - - #!/bin/bash - -#Add group to Server admin - -sudo usermod -aG docker "$user" - -#Check the group - -id "$user" - - - - - - - - 업데이트 시 해당 절차의 1.로 돌아가 명령어를 실행 합니다. - - - - Dockerfile과 docker-compose.yml - - Dockerfile 이란 - - - - 도커 이미지를 만들기 위한 스크립트로 다음과 같은 형식을 통해 작성됩니다. - - - - - - # 1. FROM: 베이스 이미지 지정 (필수) - -# - 어떤 OS 또는 미리 만들어진 이미지를 기반으로 시작할지 결정합니다. - -# - 예: ubuntu:22.04, node:20, python:3.10-slim, mariadb:latest 등 - -FROM - -# 2. WORKDIR: 작업 디렉토리 설정 - -# - 컨테이너 내부에서 작업할 경로를 지정합니다. 이후의 COPY, RUN, CMD 명령어는 이 디렉토리에서 실행됩니다. - -WORKDIR /app - -# 3. COPY: 파일 복사 - -# - 호스트(내 컴퓨터)의 파일을 컨테이너 내부로 복사합니다. - -# - 예: COPY . . (현재 디렉토리의 모든 파일 복사) - -COPY - -# 4. RUN: 명령어 실행 - -# - 이미지 빌드 중에 실행할 명령어를 지정합니다. - -# - 예: RUN apt-get update && apt-get install -y vim (패키지 설치) - -# - 예: RUN npm install (의존성 설치) - -# - 하나의 RUN 명령어로 묶어서 빌드 레이어를 줄이면 이미지 크기 최적화에 도움 - -# - 만약 패키지를 설치 했다면, 캐시는 삭제하는 게 좋다. && rm -rf /var/lib/apt/lists/* - -RUN - -# 5. EXPOSE: 포트 노출 (문서화 목적) - -# - 컨테이너가 어떤 포트를 사용할지 명시합니다. 실제 포트 연결은 'docker run -p' 또는 'docker-compose.yml'에서 합니다. - -EXPOSE - -# 6. CMD: 컨테이너 실행 시 명령어 - -# - 컨테이너가 시작될 때 실행될 명령어입니다. - -# - Dockerfile에 하나만 존재할 수 있습니다. - -# - 예: CMD ["npm", "start"] - -# - CMD나 ENTRYPOINT는 기본 이미지의 것을 그대로 사용합니다. - -# - CMD:docker run에서 다른 명령 주면 덮어 쓰여짐. (기본 명령) - -# - ENTRYPOINT: 어떤 일이 있어도 항상 먼저 실행되는 실행 파일 - -# - 필요하다면 재정의할 수 있지만, 대부분은 건드리지 않습니다. - -CMD ["executable", "param1", "param2"] - -# 7. ENV: 환경 변수 설정 - -# - 컨테이너 내부에서 사용할 환경 변수를 지정합니다. - -ENV = - - - - - - - - Docker 파일로 실행된 이미지는 상위 이미지의 업데이트 정보를 알 수 없기 때문에, yml 파일에서 label을 추가하여 diun을 통해 관리합니다. - - - - - - labels: - - - "diun.enable=true" - - - "diun.watch_repo=Original_Image" - - - "diun.watch_tag=latest" - - - - diun 프로세스는 실행되면서 해당 도커를 감시합니다.  - - diun은 외부 원격 서버에 있는 도커도 감시할 수 있습니다. 다만 IPTABLES를 통한 명시적인 허가를 해줘야 합니다. [Docker API의 기본 포트: 2375(평문), 2376(ssl/tls)] - - 평문 전송은 공격에 취약할 수 있으므로, SSL/TLS를 적용합니다. - - - - - - - - docker-compose.yml - - - - 도커 이미지를 컨테이너로 만들 때 필요한 설정들의 모음입니다. 다음과 같이 정의합니다. - - - - - - # 1. version: Compose 파일의 버전 - -version: '3.8' - -# 2. services: 컨테이너 목록 - -services: - - # 서비스 이름 (컨테이너 이름과 다를 수 있지만, 네트워크 내부에서 식별자로 사용됨) - - : - - # 3. image: 사용할 이미지 - - # - Docker Hub에서 이미지를 가져옴 - - image: : - - - - # 4. build: Dockerfile로 이미지를 빌드 - - # - 현재 디렉토리의 Dockerfile을 사용 - - build: . - - # - 또는 특정 디렉토리의 Dockerfile을 사용 - - # build: ./my-app - - - - # 5. container_name: 컨테이너에 이름 지정 - - container_name: - - - - # 6. ports: 포트 포워딩 - - # - '호스트포트:컨테이너포트' - - ports: - - - "80:80" - - - - # 7. volumes: 데이터 영속성을 위한 볼륨 마운트 - - # - '호스트경로:컨테이너경로' - - volumes: - - - ./data:/var/lib/mysql - - - ./nginx.conf:/etc/nginx/nginx.conf - - - - # 8. environment: 환경 변수 설정 - - environment: - - - DB_USER=myuser - - - DB_PASSWORD=mypassword - - # 9. depends_on: 서비스 간 종속성 설정 - - # - DB 컨테이너가 먼저 시작되어야 Node.js 앱이 실행되도록 설정 - - depends_on: - - - db - -# 10. networks: 네트워크 설정 (선택 사항) - -networks: - - : - - driver: bridge - - - -# 11. volumes: 볼륨 정의 (선택 사항) - -volumes: - - : - - - - - - - - - - Docker 명령어 - - Docker image 명령어 - - - - docker images [ls] - - - - 로컬 도커 이미지 목록 확인 - - - - - - docker rmi 이미지 id[:리포지토리:태그명] - - - - 도커 이미지 삭제, id를 통한 삭제 권장 - - - - - - docker rmi prune - - - - 사용되지 않는 모든 도커 이미지 삭제 - - - - - - docker image bulid -t 이미지명[:태그명] path - - - - Dockerfile 이미지 생성, 도커 파일은 path의 root에 위치 - - - - - - docker image pull 레파지토리명[:태그명] - - - - docker 이미지를 다운로드 - - - - - - - - Docker container 명령어 - - - - - - docker ps - - - - - - 실행중인 컨테이너 목록 확인 - - - - - - - - - - docker ps -a - - - - - - 전체 컨테이너 목록 확인 - - - - - - - - - - docker container ls -a - - - - - - 전체 컨테이너 목록 확인 - - - - - - - - - - docker start 컨테이너명[:컨테이너 id] - - - - - - 컨테이너 시작 - - - - - - - - - - docker attach 컨테이너명[:컨테이너 id] - - - - - - 컨테이너 접속 - - - - - - - - - - docker stop 컨테이너명[:컨테이너 id] - - - - - - 컨테이너 멈춤 - - - - - - - - - - docker run [옵션] 이미지명  - - - - - - 컨테이너 생성 및 시작 - - - - -d: 백그라운드 실행 - - --name: 컨테이너 이름 지정 - - -p [host_port:container_port]: 호스트와 컨테이너가 연결될 포트 지정 - - -v [host_directory]:[container_directory]: 마운트 될 볼륨 지정 - - --rm: 일회성으로 실행 - - -it 이미지명 [쉘]: 상호작용 모드 활성화 및 가상 터미널 할당 - - - - - - - - docker exec -it 컨테이너명[:컨테이너 id] [쉘] - - - - - - 이미 실행 중인 컨테이너 접속 - - - - - - - - - - docker rm 컨테이너명[:컨테이너 id] - - - - - - 컨테이너 삭제 - - - - - - - - - - exit : 컨테이너 빠져나오기 - - - - - - 결과 - - - - Docker Engine 설치 - - Docker Compose 설치 - - 서버 관리자 계정에 Docker 그룹 추가 - - - - - - 2025-06-27 - 초안 작성 완료 - - 2025-06-30 - dockerfile과 docker-compose.yml 내용 추가 - -부록 1. 문서 작성 가이드라인 및 용어집 - -부록 1.1. 문서 작성 가이드라인 -목표 - - 이 가이드라인은 해당 프로젝트의 문서 품질을 향상시키고, 모든 내용을 명확하고 일관되게 작성하기 위한 기준 문서입니다. 개인 프로젝트로 작성자 1인을 위한 가이드라인이며, 추후 작성자 및 예상 독자(기술면접관 등)를 위하여 다음과 같은 목표를 추구합니다. - - - - 정확성: 모든 정보는 사실에 기반하여 이해하기 쉽도록 작성합니다. - - 간결성: 명확하고 간결한 문장을 사용하여 핵심 내용을 전달합니다. - - 최신성: 모든 진행 사항은 문서에 업데이트 되어 항상 최신 상태를 유지합니다. - - - - 문서 구조 및 형식 - - 제목 계층 - - 모든 페이지는 H1, H2, H3 등의 HTML 제목 태그를 일관되게 사용하여 내용의 계층 구조를 명확하게 합니다.  - - 목차 - - BookStack의 기본 기능을 사용하여 관리합니다. - - 날짜 표기 - - 날짜의 표기는 국제 표준 ISO 8061에 따라 YYYY-MM-DD hh:mm:ss로 표기합니다. - - 변경 이력 표시 - - BookStack의 기본 기능과 별개로 페이지 하단 구분선으로 구분된 영역에 문서 변경 이력(날짜 및 주요 변경 내용)을 기록하여 버전 관리를 합니다. 변경 이력은 다음과 같이 기록합니다. - - YYYY-MM-DD - 변경 내용 요약 - - 단락 및 목록 - - 하나의 단락에는 하나의 주제를 담습니다. 정보를 나열할 때는 순서 있는 목록(
    ) 혹은 순서 없는 목록 (