This file is a merged representation of the entire codebase, combined into a single document by Repomix. This section contains a summary of this file. This file contains a packed representation of the entire repository's contents. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes. The content is organized as follows: 1. This summary section 2. Repository information 3. Directory structure 4. Repository files (if enabled) 5. Multiple file entries, each consisting of: - File path as an attribute - Full contents of the file - This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version. - When processing this file, use the file path to distinguish between different files in the repository. - Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. - Some files may have been excluded based on .gitignore rules and Repomix's configuration - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files - Files matching patterns in .gitignore are excluded - Files matching default ignore patterns are excluded - Files are sorted by Git change count (files with more changes are at the bottom) ansible/ inventory/ group_vars/ all.yml .gitkeep hosts.example.yml playbooks/ .gitkeep bf-collect-logs.yml bf-deploy-update.yml bf-disk-check.yml bf-docker-status.yml bf-gui-off.yml bf-gui-on.yml bf-package-check.yml bf-ping.yml bf-rdp-restart.yml bf-reboot.yml bf-run-script.yml bf-rustdesk-restart.yml bf-security-updates-check.yml bf-service-restart.yml bf-uptime.yml bf-wireguard-restart.yml roles/ common/ defaults/ main.yml tasks/ main.yml README.md docker/ defaults/ main.yml tasks/ main.yml README.md monitoring/ defaults/ main.yml tasks/ main.yml README.md wireguard/ defaults/ main.yml handlers/ main.yml tasks/ main.yml README.md .gitkeep config/ docker/ .gitkeep daemon.json firewall/ .gitkeep blueforce-docker-user-restore.sh ufw-blueforce.sh ssh/ sshd_config.d/ blueforce.conf .gitkeep systemd/ docker.service.d/ blueforce-docker-firewall.conf wg-quick@wg0.service.d/ override.conf .gitkeep bf-gui-off.service bf-gui-on.service blueforce-docker-firewall.service journald-blueforce.conf logrotate-blueforce wireguard/ .gitkeep README.md wg0.conf.example docs/ _TEMPLATE.md 00-MASTER-PLAN.md 01-ARCHITECTURE.md 02-DEVICE-NAMING-AND-INVENTORY.md 03-UBUNTU-BASELINE.md 04-GOLDEN-IMAGE-AND-PROVISIONING.md 05-ONE-CLICK-INSTALLER.md 06-USERS-SSH-AND-PERMISSIONS.md 07-REMOTE-ACCESS.md 08-WIREGUARD-AND-NETWORK.md 09-FLEET-MANAGEMENT.md 10-UPDATE-AND-ROLLBACK-POLICY.md 11-SECURITY-HARDENING.md 12-DOCKER-OPERATIONS.md 13-MEG-LINUX-ACCEPTANCE.md 14-MONITORING-AND-HEALTH.md 15-LOGGING.md 16-POWER-LOSS-AND-AUTO-RECOVERY.md 17-BACKUP-RECOVERY-AND-REINSTALL.md 18-PILOT-AND-700-DEVICE-ROLLOUT.md 19-TROUBLESHOOTING.md 20-FIELD-TECHNICIAN-RUNBOOK.md 21-CENTRAL-ADMIN-RUNBOOK.md 22-DOCUMENTATION-PLATFORM.md 23-SECURITY-AND-LICENSE-AUDIT.md 24-DECISION-LOG.md 25-IMPLEMENTATION-ROADMAP.md docs-site/ docs/ .gitkeep src/ css/ custom.css pages/ index.tsx .gitignore .gitkeep babel.config.js docusaurus.config.ts package.json README.md sidebars.js sync-docs.sh tsconfig.json monitoring/ grafana/ dashboards/ fleet-overview.json provisioning/ dashboards/ dashboards.yml datasources/ prometheus.yml prometheus/ targets/ fleet.json README.md alerts.yml prometheus.yml .gitkeep docker-compose.yml README.md research/ .gitkeep A-os-gui-wireguard-docker.md B-remote-access.md C-fleet-monitoring-docs.md rustdesk/ .gitkeep docker-compose.yml README.md scripts/ diagnostics/ .gitkeep bf-diagnostics bf-hardware-inventory.sh bf-status bf-support-bundle install/ installer/ modules/ 01-precheck.sh 02-system.sh 03-user.sh 04-hostname.sh 05-network.sh 06-wireguard.sh 07-ssh.sh 08-rdp.sh 09-rustdesk.sh 10-docker.sh 11-meg.sh 12-monitoring.sh 13-firewall.sh 14-update-policy.sh 15-logrotate.sh 16-gui.sh 17-healthcheck.sh 18-final-check.sh .gitkeep blueforce-install.sh README.md maintenance/ .gitkeep bf-gui-off bf-gui-on recovery/ .gitkeep tests/ .gitkeep check-configs.sh check-specs.sh semaphore-community-limits.md .gitignore README.md SUMMARY.md This section contains the contents of the repository's files. # Shared variables for all Blueforce fleet hosts. # Fill placeholders before first production run. --- # WireGuard subnet (placeholder - must match docs/08 hub topology). wg_subnet: "10.42.0.0/16" wg_interface: wg0 wg_hub_endpoint: "vpn.example.com:51820" # PLACEHOLDER: real hub endpoint # Monitoring endpoint (placeholder - VictoriaMetrics/Prometheus push or pull target). monitoring_endpoint: "http://monitoring.example.com:8428" # PLACEHOLDER monitoring_job: blueforce # Update repository / mirror (placeholder - local mirror recommended for 700 devices). update_repo_url: "http://mirror.example.com/ubuntu" # PLACEHOLDER update_repo_suite: jammy meg_image_tag: "1.0.0" # pinned tag, never 'latest' (docs/10, docs/12) # Operation policy defaults. Every fleet-changing play must bind to one approved wave. operation_wave: "" operation_confirmed: false reboot_grace_seconds: 300 support_bundle_dir: /var/lib/blueforce/support-bundles # Blueforce fleet inventory (EXAMPLE - copy to hosts.yml and fill real IDs). # Naming: bf-<8-digit-dealer-no> per docs/02-DEVICE-NAMING-AND-INVENTORY.md. # Targeting levels per docs/09-FLEET-MANAGEMENT.md: single host -> group -> pilot wave -> full fleet. # Waves per docs/10-UPDATE-AND-ROLLBACK-POLICY.md: lab(2)/pilot_1(5)/pilot_2(20)/wave_1(50)/wave_2(100)/production. all: children: blueforce: children: lab: hosts: bf-12010101: ansible_host: 10.42.0.11 bf-12010102: ansible_host: 10.42.0.12 pilot_1: hosts: bf-12010201: ansible_host: 10.42.1.11 bf-12010202: ansible_host: 10.42.1.12 bf-12010203: ansible_host: 10.42.1.13 bf-12010204: ansible_host: 10.42.1.14 bf-12010205: ansible_host: 10.42.1.15 pilot_2: hosts: bf-12010301: ansible_host: 10.42.2.11 bf-12010302: ansible_host: 10.42.2.12 wave_1: hosts: bf-12010401: ansible_host: 10.42.3.11 bf-12010402: ansible_host: 10.42.3.12 wave_2: hosts: bf-12010501: ansible_host: 10.42.4.11 bf-12010502: ansible_host: 10.42.4.12 production: hosts: bf-12010601: ansible_host: 10.42.5.11 bf-12010602: ansible_host: 10.42.5.12 vars: ansible_user: blueforce ansible_ssh_private_key_file: ~/.ssh/blueforce_id_ed25519 # PLACEHOLDER: set per-admin key path (see docs/06) ansible_python_interpreter: /usr/bin/python3 --- # Collect timestamped journald logs and a support snapshot from one approved wave. - name: Collect support logs hosts: "{{ operation_wave | default('invalid-wave', true) }}" gather_facts: false become: true vars: approved_operation_waves: [lab, pilot_1, pilot_2, wave_1, wave_2, production] production_override: false collect_logs_dir: "{{ support_bundle_dir | default('/var/lib/blueforce/support-bundles') }}" collect_log_lines: "{{ log_lines | default(500) }}" fetch_logs: false pre_tasks: - name: Require confirmed approved operation wave scope ansible.builtin.assert: that: - operation_wave in approved_operation_waves - operation_confirmed | bool - operation_wave in groups - ansible_play_hosts_all | length > 0 - ansible_play_hosts_all | difference(groups[operation_wave]) | length == 0 fail_msg: "Set operation_wave to one inventory wave and operation_confirmed=true; full-fleet scope is refused." run_once: true - name: Require a safe support bundle directory ansible.builtin.assert: that: - collect_logs_dir is match('^/var/lib/blueforce/support-bundles(?:/[A-Za-z0-9][A-Za-z0-9._-]*)*\Z') fail_msg: "support_bundle_dir must be /var/lib/blueforce/support-bundles or an approved alphanumeric subpath; shell metacharacters are refused." run_once: true - name: Require separate production override ansible.builtin.assert: that: - operation_wave != 'production' or production_override | bool fail_msg: "Production operation requires production_override=true in addition to operation_confirmed=true." run_once: true tasks: - name: Generate unique bundle timestamp ansible.builtin.command: date -u +%Y%m%dT%H%M%SZ register: bundle_timestamp changed_when: false - name: Ensure bundle directory exists ansible.builtin.file: path: "{{ collect_logs_dir }}" state: directory owner: root group: root mode: "0750" - name: Export recent journal to timestamped bundle ansible.builtin.shell: >- journalctl -n {{ collect_log_lines | int }} --no-pager > {{ (collect_logs_dir ~ '/journal-' ~ inventory_hostname ~ '-' ~ bundle_timestamp.stdout ~ '.log') | quote }} changed_when: true - name: Record timestamped kernel and disk snapshot ansible.builtin.shell: >- { uname -a; printf '%s\\n' '---'; df -h; printf '%s\\n' '---'; uptime; } > {{ (collect_logs_dir ~ '/snapshot-' ~ inventory_hostname ~ '-' ~ bundle_timestamp.stdout ~ '.txt') | quote }} changed_when: true - name: Fetch timestamped journal bundle to controller ansible.builtin.fetch: src: "{{ collect_logs_dir }}/journal-{{ inventory_hostname }}-{{ bundle_timestamp.stdout }}.log" dest: "fetched-logs/{{ inventory_hostname }}/" flat: true when: fetch_logs | bool --- # Approved package update with mandatory wave scope and per-batch health gates. # Example: ansible-playbook -i inventory/hosts.yml playbooks/bf-deploy-update.yml -e release_wave=lab -e wave_gate_confirmed=true -e update_packages='["package=version"]' - name: Deploy approved package update hosts: "{{ release_wave | default('invalid-wave', true) }}" gather_facts: false become: true serial: 5 max_fail_percentage: 0 any_errors_fatal: true vars: release_wave: "" wave_gate_confirmed: false production_override: false halt_on_fail: true update_packages: [] update_cache_time: 3600 approved_waves: [lab, pilot_1, pilot_2, wave_1, wave_2, production] pre_tasks: - name: Require a valid rollout wave ansible.builtin.assert: that: - release_wave in approved_waves - release_wave in groups - groups[release_wave] | length > 0 fail_msg: "release_wave must name a non-empty approved inventory group; refusing an ungated rollout." run_once: true - name: Require exact or subset host selection for release wave ansible.builtin.assert: that: - ansible_play_hosts_all | length > 0 - ansible_play_hosts_all | difference(groups[release_wave]) | length == 0 fail_msg: "Selected hosts must equal or be a subset of groups[release_wave]; broader fleet scope is refused." run_once: true - name: Require central wave-gate confirmation ansible.builtin.assert: that: - wave_gate_confirmed | bool fail_msg: "Set wave_gate_confirmed=true only after the prior wave health gate and central approval pass." run_once: true - name: Require separate production override ansible.builtin.assert: that: - release_wave != 'production' or production_override | bool fail_msg: "Production rollout requires production_override=true in addition to wave_gate_confirmed=true." run_once: true - name: Require halt-on-failure protection ansible.builtin.assert: that: - halt_on_fail | bool fail_msg: "halt_on_fail must remain true for every rollout." run_once: true - name: Require pinned package versions ansible.builtin.assert: that: - update_packages | length > 0 - update_packages | select('match', '^[A-Za-z0-9][A-Za-z0-9+_.:-]*=.+$') | list | length == update_packages | length fail_msg: "update_packages must be a non-empty list of exact package=version pins." run_once: true tasks: - name: Update apt cache ansible.builtin.apt: update_cache: true cache_valid_time: "{{ update_cache_time }}" - name: Install approved pinned package versions ansible.builtin.apt: name: "{{ update_packages }}" state: present allow_downgrade: true post_tasks: - name: Run mandatory post-batch health gate ansible.builtin.command: /usr/local/bin/bf-status register: update_health changed_when: false failed_when: update_health.rc != 0 - name: Report passed post-batch health gate ansible.builtin.debug: msg: "Update batch health gate passed for {{ ansible_play_batch }}" run_once: true --- # Check disk usage (read-only) and warn above threshold. # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-disk-check.yml --limit lab # Threshold overridable: -e disk_warn_pct=80. # No Semaphore Pro feature is used. - name: Check disk usage hosts: blueforce gather_facts: false vars: disk_warning_percent: "{{ disk_warn_pct | default(80) }}" tasks: - name: Get disk usage ansible.builtin.command: df -h --output=source,pcent,target register: df_out changed_when: false - name: Show disk usage ansible.builtin.debug: msg: "{{ inventory_hostname }}:\n{{ df_out.stdout }}" - name: Get root filesystem usage percentage ansible.builtin.shell: "df --output=pcent / | tail -n 1 | tr -dc '0-9'" register: root_pct changed_when: false - name: Flag hosts above threshold ansible.builtin.debug: msg: "WARNING {{ inventory_hostname }} root disk {{ root_pct.stdout }}% exceeds {{ disk_warning_percent }}%" when: root_pct.stdout | int >= (disk_warning_percent | int) --- # Report Docker daemon and container status (read-only). # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-docker-status.yml --limit lab # No changes are made by this playbook. No Semaphore Pro feature is used. - name: Check Docker status hosts: blueforce gather_facts: false become: true tasks: - name: Check Docker service state ansible.builtin.systemd: name: docker register: docker_svc changed_when: false - name: List running containers ansible.builtin.command: docker ps --format '{{{{.Names}}}} {{{{.Image}}}} {{{{.Status}}}}' register: docker_ps changed_when: false failed_when: false - name: Show Docker status ansible.builtin.debug: msg: "{{ inventory_hostname }} docker={{ docker_svc.status.ActiveState | default('unknown') }} containers={{ docker_ps.stdout_lines | default([]) }}" --- # Disable the GUI for a confirmed maintenance wave (kiosk/headless mode). # Dry run: ansible-playbook -i inventory/hosts.yml playbooks/bf-gui-off.yml -e operation_wave=lab -e operation_confirmed=true --check - name: Disable graphical target hosts: "{{ operation_wave | default('invalid-wave', true) }}" gather_facts: false become: true vars: approved_operation_waves: [lab, pilot_1, pilot_2, wave_1, wave_2, production] production_override: false pre_tasks: - name: Require confirmed approved operation wave scope ansible.builtin.assert: that: - operation_wave in approved_operation_waves - operation_confirmed | bool - operation_wave in groups - ansible_play_hosts_all | length > 0 - ansible_play_hosts_all | difference(groups[operation_wave]) | length == 0 fail_msg: "Set operation_wave to one inventory wave and operation_confirmed=true; full-fleet scope is refused." run_once: true - name: Require separate production override ansible.builtin.assert: that: - operation_wave != 'production' or production_override | bool fail_msg: "Production operation requires production_override=true in addition to operation_confirmed=true." run_once: true tasks: - name: Read current default target ansible.builtin.command: systemctl get-default register: current_default_target changed_when: false - name: Set default target to multi-user ansible.builtin.command: systemctl set-default multi-user.target when: current_default_target.stdout != 'multi-user.target' - name: Stop graphical target now ansible.builtin.systemd: name: graphical.target state: stopped --- # Enable the GUI for a confirmed maintenance wave without changing the boot default. - name: Enable graphical maintenance session hosts: "{{ operation_wave | default('invalid-wave', true) }}" gather_facts: false become: true vars: approved_operation_waves: [lab, pilot_1, pilot_2, wave_1, wave_2, production] production_override: false pre_tasks: - name: Require confirmed approved operation wave scope ansible.builtin.assert: that: - operation_wave in approved_operation_waves - operation_confirmed | bool - operation_wave in groups - ansible_play_hosts_all | length > 0 - ansible_play_hosts_all | difference(groups[operation_wave]) | length == 0 fail_msg: "Set operation_wave to one inventory wave and operation_confirmed=true; full-fleet scope is refused." run_once: true - name: Require separate production override ansible.builtin.assert: that: - operation_wave != 'production' or production_override | bool fail_msg: "Production operation requires production_override=true in addition to operation_confirmed=true." run_once: true tasks: - name: Start GUI and wait for RDP ansible.builtin.command: /usr/local/bin/bf-gui-on register: gui_on changed_when: true - name: Verify terminal boot default remains configured ansible.builtin.command: systemctl get-default register: current_default_target changed_when: false failed_when: current_default_target.stdout != 'multi-user.target' --- # Report installed versions of pinned packages (read-only). # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-package-check.yml --limit lab # Package list overridable: -e "check_packages=['docker-ce','wireguard']". # No Semaphore Pro feature is used. - name: Check installed package versions hosts: blueforce gather_facts: false become: true vars: check_packages: - docker-ce - wireguard - rustdesk tasks: - name: Query package versions ansible.builtin.shell: "dpkg-query -W -f='${Package} ${Version}\n' {{ check_packages | join(' ') }}" register: pkg_out changed_when: false failed_when: false - name: Show package versions ansible.builtin.debug: msg: "{{ inventory_hostname }}:\n{{ pkg_out.stdout | default('(query failed)') }}" --- # Connectivity check for the Blueforce fleet. # Targeting: single host -> group -> pilot wave -> full fleet (--limit). # Dry run first: ansible-playbook -i inventory/hosts.yml playbooks/bf-ping.yml --limit lab --check # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-ping.yml --limit lab # No changes are made by this playbook. No Semaphore Pro feature is used. - name: Ping fleet hosts hosts: blueforce gather_facts: false tasks: - name: Check SSH connectivity ansible.builtin.ping: --- # Restart the RDP service (xrdp). # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-rdp-restart.yml --limit lab # Service name is overridable via rdp_service_name. # No Semaphore Pro feature is used. - name: Restart RDP service hosts: blueforce gather_facts: false become: true vars: rdp_restart_service: "{{ rdp_service_name | default('xrdp') }}" tasks: - name: Restart RDP ansible.builtin.systemd: name: "{{ rdp_restart_service }}" state: restarted - name: Verify RDP is active ansible.builtin.command: "systemctl is-active {{ rdp_restart_service }}" register: rdp_state changed_when: false - name: Show RDP state ansible.builtin.debug: msg: "{{ inventory_hostname }} rdp={{ rdp_state.stdout }}" --- # Controlled reboots require an approved wave and explicit confirmation. - name: Controlled fleet reboot hosts: blueforce gather_facts: false become: true serial: 2 max_fail_percentage: 0 any_errors_fatal: true vars: release_wave: "" reboot_confirmed: false allow_production_reboot: false reboot_delay_seconds: "{{ reboot_grace_seconds | default(300) }}" pre_tasks: - name: Require an approved non-production release wave ansible.builtin.assert: that: - release_wave in ['lab', 'pilot_1', 'pilot_2', 'wave_1', 'wave_2', 'production'] - reboot_confirmed | bool - release_wave != 'production' or allow_production_reboot | bool fail_msg: "Set approved release_wave and reboot_confirmed=true. Production additionally requires allow_production_reboot=true." - name: Refuse full-fleet scope without explicit production override ansible.builtin.assert: that: - release_wave == 'production' or ansible_limit | default('') | regex_search('(^|[:,])' ~ release_wave ~ '($|[:,])') fail_msg: "Limit this reboot to the approved release_wave group; full fleet is refused." tasks: - name: Reboot host with grace period ansible.builtin.reboot: reboot_timeout: 600 pre_reboot_delay: "{{ reboot_delay_seconds }}" msg: Blueforce scheduled reboot via Ansible - name: Verify host health after reboot batch ansible.builtin.command: /usr/local/bin/bf-status --check register: reboot_health changed_when: false failed_when: reboot_health.rc != 0 --- # Run an approved script already present on a confirmed maintenance wave. - name: Run operator script hosts: "{{ operation_wave | default('invalid-wave', true) }}" gather_facts: false become: true vars: approved_operation_waves: [lab, pilot_1, pilot_2, wave_1, wave_2, production] production_override: false script_path: "" script_args: "" script_allowed_dirs: - /opt/blueforce/bin - /usr/local/sbin script_allowlist: [] pre_tasks: - name: Require confirmed approved operation wave scope ansible.builtin.assert: that: - operation_wave in approved_operation_waves - operation_confirmed | bool - operation_wave in groups - ansible_play_hosts_all | length > 0 - ansible_play_hosts_all | difference(groups[operation_wave]) | length == 0 fail_msg: "Set operation_wave to one inventory wave and operation_confirmed=true; full-fleet scope is refused." run_once: true - name: Require separate production override ansible.builtin.assert: that: - operation_wave != 'production' or production_override | bool fail_msg: "Production operation requires production_override=true in addition to operation_confirmed=true." run_once: true tasks: - name: Canonicalize requested script path ansible.builtin.command: argv: [readlink, -f, --, "{{ script_path }}"] register: canonical_script changed_when: false failed_when: canonical_script.rc != 0 - name: Canonicalize approved directory prefixes ansible.builtin.command: argv: [readlink, -f, --, "{{ item }}"] loop: "{{ script_allowed_dirs }}" register: canonical_dirs changed_when: false - name: Validate exact approved path boundary or explicit allowlist ansible.builtin.assert: that: - canonical_script.stdout is match('^/.+') - canonical_script.stdout in script_allowlist or (canonical_dirs.results | map(attribute='stdout') | select('match', '^/.+') | map('regex_replace', '$', '/') | select('in', canonical_script.stdout + '/') | list | length > 0) fail_msg: "Refused root execution of '{{ script_path }}': canonical path '{{ canonical_script.stdout }}' is not in an approved directory or explicit allowlist." - name: Check canonical script is a regular executable file ansible.builtin.stat: path: "{{ canonical_script.stdout }}" register: script_stat - name: Validate canonical script metadata ansible.builtin.assert: that: - script_stat.stat.exists - script_stat.stat.isreg - script_stat.stat.executable - name: Execute approved canonical script ansible.builtin.command: argv: "{{ [canonical_script.stdout] + (script_args | default('') | split()) }}" register: script_out changed_when: true - name: Show script output ansible.builtin.debug: msg: "{{ inventory_hostname }} exit={{ script_out.rc }}\n{{ script_out.stdout | default('') }}{{ script_out.stderr | default('') }}" --- # Restart the RustDesk remote-access service. # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-rustdesk-restart.yml --limit lab # Service name is overridable via rustdesk_service_name. # No Semaphore Pro feature is used. - name: Restart RustDesk service hosts: blueforce gather_facts: false become: true vars: rustdesk_restart_service: "{{ rustdesk_service_name | default('rustdesk') }}" tasks: - name: Restart RustDesk ansible.builtin.systemd: name: "{{ rustdesk_restart_service }}" state: restarted - name: Verify RustDesk is active ansible.builtin.command: "systemctl is-active {{ rustdesk_restart_service }}" register: rustdesk_state changed_when: false - name: Show RustDesk state ansible.builtin.debug: msg: "{{ inventory_hostname }} rustdesk={{ rustdesk_state.stdout }}" --- # Report pending security updates only. NEVER installs anything. # Policy docs/10: unattended upgrades are disabled; every change needs an approval wave. # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-security-updates-check.yml --limit lab # No Semaphore Pro feature is used. - name: Report pending security updates (no install) hosts: blueforce gather_facts: false become: true tasks: - name: Refresh apt metadata (cache only, no upgrade) ansible.builtin.apt: update_cache: true cache_valid_time: 3600 - name: List upgradable packages ansible.builtin.shell: "apt list --upgradable 2>/dev/null" register: upgradable changed_when: false - name: List security updates only ansible.builtin.shell: "apt-get -s dist-upgrade -V | grep -i security || true" register: sec_updates changed_when: false - name: Show report ansible.builtin.debug: msg: "{{ inventory_hostname }} upgradable:\n{{ upgradable.stdout }}\nsecurity:\n{{ sec_updates.stdout | default('(none)') }}" - name: Remind operator that install is out of scope ansible.builtin.debug: msg: "REPORT ONLY - use bf-deploy-update.yml with approval waves to install anything." --- # Restart a single service by name (parametric, allowlisted). # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-service-restart.yml --limit lab -e service_name=blueforce-agent # Only names in service_allowlist are accepted; everything else fails closed. # No Semaphore Pro feature is used. - name: Restart allowlisted service hosts: blueforce gather_facts: false become: true vars: service_name: "" service_allowlist: - blueforce-agent - docker - sshd - ssh - systemd-journald - cron tasks: - name: Validate service_name is provided ansible.builtin.assert: that: - service_name | length > 0 fail_msg: "service_name is required, e.g. -e service_name=blueforce-agent" - name: Validate service_name is allowlisted ansible.builtin.assert: that: - service_name in service_allowlist fail_msg: "service_name '{{ service_name }}' is not in the allowlist. Refusing to restart arbitrary services." - name: Restart service ansible.builtin.systemd: name: "{{ service_name }}" state: restarted --- # Collect uptime from fleet hosts (read-only). # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-uptime.yml --limit pilot_1 # No changes are made by this playbook. No Semaphore Pro feature is used. - name: Report host uptime hosts: blueforce gather_facts: false tasks: - name: Get uptime ansible.builtin.command: uptime register: uptime_out changed_when: false - name: Show uptime ansible.builtin.debug: msg: "{{ inventory_hostname }}: {{ uptime_out.stdout }}" --- # Restart the WireGuard tunnel interface. # Run: ansible-playbook -i inventory/hosts.yml playbooks/bf-wireguard-restart.yml --limit lab # Uses wg_interface from group_vars and defaults to wg0. # No Semaphore Pro feature is used. - name: Restart WireGuard tunnel hosts: blueforce gather_facts: false become: true vars: wireguard_restart_interface: "{{ wg_interface | default('wg0') }}" tasks: - name: Restart WireGuard service ansible.builtin.systemd: name: "wg-quick@{{ wireguard_restart_interface }}" state: restarted - name: Verify tunnel state ansible.builtin.command: "wg show {{ wireguard_restart_interface }}" register: wg_out changed_when: false - name: Show WireGuard state ansible.builtin.debug: msg: "{{ inventory_hostname }}: {{ wg_out.stdout_lines | default([]) }}" --- # Defaults for the common role. Override in group_vars/host_vars, never hardcode secrets here. common_timezone: Europe/Istanbul common_admin_user: blueforce --- # Common baseline: users, time, unattended-upgrades OFF (docs/10), base packages. # No Semaphore Pro feature is used. - name: Ensure base packages are present ansible.builtin.apt: name: - openssh-server - python3 - curl - htop state: present update_cache: true cache_valid_time: 3600 - name: Disable automatic upgrades (policy docs/10) ansible.builtin.copy: dest: /etc/apt/apt.conf.d/20auto-upgrades owner: root group: root mode: "0644" content: | APT::Periodic::Update-Package-Lists "0"; APT::Periodic::Unattended-Upgrade "0"; - name: Mask apt-daily timers (policy docs/10) ansible.builtin.systemd: name: "{{ item }}" masked: true state: stopped loop: - apt-daily.timer - apt-daily-upgrade.timer failed_when: false - name: Set timezone ansible.builtin.timezone: name: "{{ common_timezone }}" # common role Baseline for every Blueforce host: base packages, timezone, and the docs/10 requirement that `unattended-upgrades` stays OFF with `apt-daily*.timer` masked. ## Variables | Name | Default | Purpose | |---|---|---| | `common_timezone` | `Europe/Istanbul` | System timezone | | `common_admin_user` | `blueforce` | Admin login (SSH key managed per docs/06) | ## Usage ```yaml - hosts: blueforce become: true roles: - common ``` --- # Docker is installed only when an operator supplies a verified repository and exact version. # Supported Ubuntu releases include 24.04 noble and 26.04 resolute; no release is hard-coded. docker_supported_codenames: - noble - resolute docker_apt_codename: "{{ ansible_distribution_release | default('') }}" docker_apt_repository: "" # Required: approved, already key-verified Docker APT repository. docker_engine_version: "" # Required exact version, e.g. 5:29.0.0-1~ubuntu.26.04~resolute. docker_packages: [] meg_container_name: meg meg_image_tag: 1.0.0 --- # Docker engine setup with explicit, approved repository and version only. - name: Refuse unsupported Ubuntu codename or unapproved Docker inputs ansible.builtin.assert: that: - docker_apt_codename in docker_supported_codenames - docker_apt_repository | length > 0 - docker_engine_version | length > 0 - docker_engine_version is not search('latest') - docker_packages | length > 0 - docker_packages | select('search', '(^|=).*latest($|[^[:alnum:]])') | list | length == 0 fail_msg: >- Docker installation is refused until this host has a supported codename and an explicitly approved repository, exact engine version, and exact package list. No automatic Docker update is permitted. - name: Verify approved Docker repository is configured ansible.builtin.command: "apt-cache policy docker-ce" register: docker_policy changed_when: false failed_when: docker_apt_repository not in docker_policy.stdout - name: Verify requested Docker version is available from approved repository ansible.builtin.command: "apt-cache madison docker-ce" register: docker_versions changed_when: false failed_when: docker_engine_version not in docker_versions.stdout - name: Install only the approved exact Docker packages ansible.builtin.apt: name: "{{ docker_packages }}" state: present update_cache: false - name: Enable and start Docker daemon ansible.builtin.systemd: name: docker enabled: true state: started - name: Ensure MEG container is running at pinned tag ansible.builtin.command: "docker ps --filter name={{ meg_container_name }} --format '{{{{.Names}}}}'" register: meg_ps changed_when: false failed_when: false when: meg_container_name | length > 0 - name: Report MEG container state ansible.builtin.debug: msg: "{{ inventory_hostname }} meg={{ meg_ps.stdout | default('(skipped)') }}" when: meg_container_name | length > 0 # docker role Installs the pinned Docker engine, enables the daemon, and reports the MEG container state. Image and package tags are always pinned; `latest` and Watchtower-style auto-updaters are forbidden (docs/10, docs/12). ## Variables | Name | Default | Purpose | |---|---|---| | `docker_packages` | pinned `docker-ce=...` | Exact engine packages to install | | `meg_container_name` | `meg` | Container name to report on | | `meg_image_tag` | `1.0.0` | Pinned MEG image tag | ## Usage ```yaml - hosts: blueforce become: true roles: - docker ``` --- # Defaults for the monitoring role. monitoring_packages: - prometheus-node-exporter monitoring_service: prometheus-node-exporter monitoring_endpoint: http://monitoring.example.com:8428 monitoring_job: blueforce --- # Monitoring baseline: node exporter install + health signal files. # No Semaphore Pro feature is used. - name: Ensure monitoring packages are present ansible.builtin.apt: name: "{{ monitoring_packages }}" state: present update_cache: true cache_valid_time: 3600 - name: Enable and start node exporter ansible.builtin.systemd: name: "{{ monitoring_service }}" enabled: true state: started - name: Verify unattended-upgrades stay disabled (policy docs/10) ansible.builtin.shell: 'grep -E ''Unattended-Upgrade "0"'' /etc/apt/apt.conf.d/20auto-upgrades' register: unattended_check changed_when: false failed_when: false - name: Report monitoring state ansible.builtin.debug: msg: "{{ inventory_hostname }} exporter={{ monitoring_service }} unattended_off={{ unattended_check.rc == 0 }}" # monitoring role Installs and starts the node exporter and verifies the docs/10 requirement that automatic upgrades stay disabled. ## Variables | Name | Default | Purpose | |---|---|---| | `monitoring_packages` | `prometheus-node-exporter` | Packages to install | | `monitoring_service` | `prometheus-node-exporter` | Service to enable | | `monitoring_endpoint` | `http://monitoring.example.com:8428` | Metrics endpoint (placeholder) | | `monitoring_job` | `blueforce` | Job label | ## Usage ```yaml - hosts: blueforce become: true roles: - monitoring ``` --- # Defaults for the wireguard role. Secrets MUST be overridden via Key Store / vault. wg_interface: wg0 wg_address: "" wg_dns: "" wg_private_key: "" wg_hub_public_key: "" wg_hub_endpoint: "" wg_allowed_ips: "10.42.0.0/16" wg_keepalive: 25 --- # Handlers for the wireguard role. - name: Restart WireGuard ansible.builtin.systemd: name: "wg-quick@{{ wg_interface }}" state: restarted --- # WireGuard client setup: package, config from template variables, tunnel up. # Private keys are supplied through the Semaphore Community Key Store, never committed. # No Semaphore Pro feature is used. - name: Validate required WireGuard configuration ansible.builtin.assert: that: - wg_address | length > 0 - wg_private_key | length > 0 - wg_hub_public_key | length > 0 - wg_hub_endpoint | length > 0 - wg_allowed_ips | length > 0 fail_msg: WireGuard address, keys, hub endpoint, and allowed IPs are required. no_log: true - name: Ensure WireGuard package is present ansible.builtin.apt: name: wireguard state: present update_cache: true cache_valid_time: 3600 - name: Write WireGuard client config ansible.builtin.copy: dest: "/etc/wireguard/{{ wg_interface }}.conf" owner: root group: root mode: "0600" content: | [Interface] PrivateKey = {{ wg_private_key }} Address = {{ wg_address }} {% if wg_dns | default('') | length > 0 %} DNS = {{ wg_dns }} {% endif %} [Peer] PublicKey = {{ wg_hub_public_key }} Endpoint = {{ wg_hub_endpoint }} AllowedIPs = {{ wg_allowed_ips }} PersistentKeepalive = {{ wg_keepalive }} no_log: true notify: Restart WireGuard - name: Enable and start WireGuard tunnel ansible.builtin.systemd: name: "wg-quick@{{ wg_interface }}" enabled: true state: started # wireguard role Installs WireGuard, writes `/etc/wireguard/.conf` from variables, and starts `wg-quick@`. Private keys are secrets: supply them through the Semaphore Community encrypted Key Store or another controller-side secret source; never commit them to Git. ## Variables | Name | Default | Purpose | |---|---|---| | `wg_interface` | `wg0` | Interface name | | `wg_address` | _(empty)_ | Required client tunnel address, e.g. `10.42.3.11/16` | | `wg_dns` | _(empty)_ | Optional DNS | | `wg_private_key` | _(empty)_ | Required client private key (secret) | | `wg_hub_public_key` | _(empty)_ | Required hub public key | | `wg_hub_endpoint` | `vpn.example.com:51820` | Required hub endpoint | | `wg_allowed_ips` | `10.42.0.0/16` | Required allowed IPs | | `wg_keepalive` | `25` | Persistent keepalive seconds | The role fails closed when any required tunnel value is empty. ## Usage ```yaml - hosts: blueforce become: true roles: - role: wireguard vars: wg_address: 10.42.3.11/16 ``` { "_note": [ "Purpose: Cap Docker log disk use and keep containers alive across daemon restarts.", "Docker manages firewall rules; the installer applies the mandatory DOCKER-USER deny policy.", "Published application ports require explicit reviewed rules in /etc/blueforce/docker-published-port-allowlist.conf." ], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "live-restore": true, "iptables": true } #!/usr/bin/env bash # Apply Blueforce's reviewed Docker published-port policy to DOCKER-USER. set -euo pipefail ALLOWLIST="${DOCKER_ALLOWLIST:-/etc/blueforce/docker-published-port-allowlist.conf}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 fail() { printf 'blueforce-docker-firewall: %s\n' "$*" >&2; exit 1; } parse_allowlist() { local callback="$1" raw line protocol port extra [[ -r "$ALLOWLIST" ]] || fail "allowlist is missing or unreadable: $ALLOWLIST" while IFS= read -r raw || [[ -n "$raw" ]]; do line="${raw%%#*}" line="$(printf '%s' "$line" | xargs)" [[ -z "$line" ]] && continue read -r protocol port extra <<< "$line" [[ -z "${extra:-}" && "$protocol" =~ ^(tcp|udp)$ && "$port" =~ ^[0-9]{1,5}$ ]] || fail "invalid allowlist entry '$raw'; use 'tcp PORT' or 'udp PORT'" ((10#$port >= 1 && 10#$port <= 65535)) || fail "port out of range in '$raw'" "$callback" "$protocol" "$port" done < "$ALLOWLIST" } validate_allowlist() { parse_allowlist :; } install_allow_rule() { local protocol="$1" port="$2" # Docker DNAT precedes DOCKER-USER; match the original reviewed published port. iptables -I DOCKER-USER -p "$protocol" -m conntrack --ctorigdstport "$port" -j ACCEPT } verify_allow_rule() { local protocol="$1" port="$2" iptables -C DOCKER-USER -p "$protocol" -m conntrack --ctorigdstport "$port" -j ACCEPT || fail "allowlist rule missing: $protocol $port" } validate_allowlist command -v iptables >/dev/null 2>&1 || fail "iptables is required" iptables -S DOCKER-USER >/dev/null 2>&1 || fail "DOCKER-USER chain is unavailable; start Docker first" if [[ "$CHECK_MODE" -eq 1 ]]; then iptables -C DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT || fail "established/related rule missing" iptables -C DOCKER-USER -i docker0 -j RETURN || fail "docker0 outbound return rule missing" iptables -C DOCKER-USER -i br+ -j RETURN || fail "user-defined bridge outbound return rule missing" parse_allowlist verify_allow_rule iptables -C DOCKER-USER -j DROP || fail "default published-port deny rule missing" exit 0 fi # Blueforce owns DOCKER-USER so an old permissive rule cannot bypass the deny. iptables -F DOCKER-USER iptables -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # Container-to-Internet packets enter FORWARD from Docker bridges; inbound published-port # packets enter from an external interface, so these returns do not bypass the deny below. iptables -A DOCKER-USER -i docker0 -j RETURN iptables -A DOCKER-USER -i br+ -j RETURN # Insert every parsed allowlist exception before the final default drop. parse_allowlist install_allow_rule iptables -A DOCKER-USER -j DROP #!/usr/bin/env bash # Apply field-device UFW baseline. SSH/RDP are accepted only on wg0. set -euo pipefail WG_IF="wg0" WG_PORT="51820" DOCKER_ALLOWLIST="/etc/blueforce/docker-published-port-allowlist.conf" DOCKER_POLICY_BIN="/usr/local/sbin/blueforce-docker-firewall" DOCKER_POLICY_UNIT="blueforce-docker-firewall.service" rule_exists() { ufw status | grep -Fq "$1"; } ensure_rule() { local match="$1"; shift; rule_exists "$match" && echo "keep: $match" || ufw "$@"; } install_docker_policy() { local root root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" install -d -m 755 /etc/blueforce /etc/iptables if [[ ! -f "$DOCKER_ALLOWLIST" ]]; then printf '%s\n' '# Explicit reviewed Docker published-port exceptions: tcp PORT or udp PORT.' '# Default: deny every externally published Docker port.' > "$DOCKER_ALLOWLIST" fi chown root:root "$DOCKER_ALLOWLIST" chmod 640 "$DOCKER_ALLOWLIST" install -o root -g root -m 700 "$root/config/firewall/blueforce-docker-user-restore.sh" "$DOCKER_POLICY_BIN" install -o root -g root -m 644 "$root/config/systemd/blueforce-docker-firewall.service" "/etc/systemd/system/$DOCKER_POLICY_UNIT" systemctl daemon-reload systemctl enable "$DOCKER_POLICY_UNIT" systemctl start docker "$DOCKER_POLICY_BIN" } cmd_apply() { ufw default deny incoming ufw default allow outgoing ensure_rule "${WG_PORT}/udp" allow "${WG_PORT}/udp" comment 'WireGuard hub' ensure_rule "22/tcp on ${WG_IF}" allow in on "${WG_IF}" to any port 22 proto tcp comment 'SSH over WireGuard' ensure_rule "3389/tcp on ${WG_IF}" allow in on "${WG_IF}" to any port 3389 proto tcp comment 'RDP over WireGuard' install_docker_policy ufw --force enable ufw status verbose } cmd_reset() { ufw --force reset; ufw default deny incoming; ufw default allow outgoing; ufw --force enable; ufw status verbose; } cmd_status() { ufw status verbose; "$DOCKER_POLICY_BIN" --check; iptables -S DOCKER-USER; } case "${1:-apply}" in apply) cmd_apply ;; reset) cmd_reset ;; status) cmd_status ;; *) echo "Usage: $0 {apply|reset|status}" >&2; exit 1 ;; esac # Purpose: Harden field-device SSH to key-only maintenance access. # Scope: Applies to the blueforce user; root and password logins stay disabled. # Safety: Drop-in for sshd_config.d; validate with sshd -t before reload. # Ref: docs/06-USERS-SSH-AND-PERMISSIONS.md, K-13. # No direct root login; privilege is gained only via audited sudo. PermitRootLogin no # Key-only auth: passwords and interactive challenges are disabled. PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes # Only the maintenance user may open SSH sessions. AllowUsers blueforce # Reduce idle-session and brute-force exposure. X11Forwarding no MaxAuthTries 3 LoginGraceTime 60 ClientAliveInterval 300 ClientAliveCountMax 2 # Start the firewall after every Docker start; the firewall unit orders itself after Docker. [Unit] Wants=blueforce-docker-firewall.service # Purpose: Keep the WireGuard tunnel up across failures without manual action. # Scope: Drop-in for wg-quick@wg0; restarts the tunnel with a short backoff. # Safety: Reboot-safe once the unit is enabled; confirm on the LAB 26.04.1 image. # Ref: docs/08-WIREGUARD-AND-NETWORK.md and docs/16-POWER-LOSS-AND-AUTO-RECOVERY.md, K-05. [Service] Restart=always RestartSec=5 # Purpose: Manually return a field device to terminal mode after maintenance. # This unit is deliberately not enabled by any boot target. [Unit] Description=Blueforce GUI off (manual terminal maintenance switch) After=graphical.target [Service] Type=oneshot ExecStart=/usr/local/bin/bf-gui-off RemainAfterExit=no # Purpose: Manually switch a field device to graphical mode for maintenance. # This unit is deliberately not enabled by any boot target. [Unit] Description=Blueforce GUI on (manual graphical maintenance switch) After=multi-user.target [Service] Type=oneshot ExecStart=/usr/local/bin/bf-gui-on RemainAfterExit=no [Unit] Description=Blueforce persistent Docker published-port firewall BindsTo=docker.service PartOf=docker.service After=docker.service [Service] Type=oneshot ExecStart=/usr/local/sbin/blueforce-docker-firewall RemainAfterExit=yes [Install] WantedBy=multi-user.target # Purpose: Bound journald disk use on field devices while keeping history. # Scope: Persistent storage with caps; install to /etc/systemd/journald.conf.d/. # Safety: Reboot-safe; apply with systemctl restart systemd-journald. # Ref: docs/15-LOGGING.md, K-14. [Journal] Storage=persistent SystemMaxUse=500M RuntimeMaxUse=100M MaxFileSec=14day # Purpose: Rotate MEG and Blueforce application logs to cap disk use. # Scope: /var/log/blueforce and /var/log/meg file logs; install to /etc/logrotate.d/. # Safety: Idempotent under logrotate; validate with logrotate -d before deploying. # Ref: docs/15-LOGGING.md, K-14. /var/log/blueforce/*.log /var/log/meg/*.log { weekly rotate 7 size 10M maxage 30 compress delaycompress missingok notifempty create 0640 root adm sharedscripts postrotate /usr/bin/systemctl kill --signal=SIGHUP rsyslog.service >/dev/null 2>&1 || true endscript } # NOTE: install.log and update-history.log stay append-only and are excluded here. # WireGuard spoke reference Ref: `docs/08-WIREGUARD-AND-NETWORK.md` (K-05), naming `K-12`. ## Layout - Hub-spoke: the central server is the hub (`10.8.0.1`), each field device is a spoke. - Peer name equals the device identity: `bf-<8digits>` (hostname, inventory, monitoring label). - Each spoke gets one fixed tunnel IP (`10.8.0.x/32`) from inventory. ## Filling the example 1. Copy `wg0.conf.example` to `/etc/wireguard/wg0.conf` (mode `600`). 2. Replace `Address` with the device tunnel IP from inventory. 3. Replace `` with the device private key. 4. Replace `` and `` with hub values. 5. Keep `PersistentKeepalive = 25` (official reasonable default for NAT peers). 6. Keep `PostUp`/`PostDown`: they scope SSH (22) and RDP (3389) rules to `wg0`. ## Bring-up ```bash sudo install -m 600 wg0.conf /etc/wireguard/wg0.conf sudo systemctl enable --now wg-quick@wg0 sudo wg show wg0 latest-handshakes ``` ## Notes - Spokes behind CGNAT must always initiate outward; the hub only listens on `51820/udp`. - Placeholders marked `` must never hold real keys in this repo. # Purpose: Reference spoke config for field devices joining the hub. # Scope: Peer name bf- with fixed tunnel IP; hub values come from inventory. # Safety: Example only; replace every placeholder before installing to /etc/wireguard/. # Ref: docs/08-WIREGUARD-AND-NETWORK.md, K-05; peer naming K-12. [Interface] # Tunnel address from inventory (one fixed /32 per device, hub is 10.8.0.1). Address = 10.8.0.15/32 # Device private key. with the real key; never commit it. PrivateKey = # Open management ports on the tunnel interface when it comes up. PostUp = ufw allow in on wg0 to any port 22 proto tcp; ufw allow in on wg0 to any port 3389 proto tcp # Remove tunnel-scoped management rules when the tunnel goes down. PostDown = ufw delete allow in on wg0 to any port 22 proto tcp; ufw delete allow in on wg0 to any port 3389 proto tcp [Peer] # Hub public key. with the real hub key. PublicKey = # Hub endpoint. with the real hub address. Endpoint = :51820 # Return traffic is limited to the tunnel subnet. AllowedIPs = 10.8.0.0/16 # Keep the NAT/CGNAT mapping alive behind mobile networks. PersistentKeepalive = 25 Generated by ../sync-docs.sh; do not edit by hand. /* Blueforce docs theme overrides. Dark mode is the default (see docusaurus.config.ts). */ :root { --ifm-color-primary: #4aa8ff; } import React from 'react'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; export default function Home() { const {siteConfig} = useDocusaurusContext(); return (

{siteConfig.title}

{siteConfig.tagline}

Open docs
); }
node_modules/ build/ .docusaurus/ # Generated content (recreated by sync-docs.sh on prestart/prebuild). docs/*.md !docs/.gitkeep module.exports = { presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; import {themes as prismThemes} from 'prism-react-renderer'; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; // Blueforce fleet documentation site. Content source: ../docs/*.md. const config: Config = { title: 'Blueforce Docs', tagline: 'Fleet runbooks and architecture', url: 'http://docs.blueforce.intranet', baseUrl: '/', onBrokenLinks: 'throw', i18n: { defaultLocale: 'tr', locales: ['tr'] }, markdown: { mermaid: true, hooks: { onBrokenMarkdownLinks: 'warn' }, }, themes: ['@docusaurus/theme-mermaid', '@easyops-cn/docusaurus-search-local'], presets: [ ['classic', { docs: { path: 'docs', routeBasePath: '/', sidebarPath: './sidebars.js' }, blog: false, theme: { customCss: './src/css/custom.css' }, } satisfies Preset.Options], ], themeConfig: { colorMode: { defaultMode: 'dark', disableSwitch: false, respectPrefersColorScheme: false }, navbar: { title: 'Blueforce Docs', items: [{ type: 'docSidebar', sidebarId: 'docs', position: 'left', label: 'Docs' }] }, } satisfies Preset.ThemeConfig, }; export default config; { "name": "blueforce-docs", "version": "0.1.0", "private": true, "description": "Blueforce fleet documentation site. Source of truth is ../docs/*.md; this package only builds the site.", "scripts": { "sync": "./sync-docs.sh", "prestart": "./sync-docs.sh", "prebuild": "./sync-docs.sh", "start": "docusaurus start --host 127.0.0.1 --port 3000", "start:intranet": "docusaurus start --host 0.0.0.0 --port 3000", "build": "docusaurus build", "serve": "docusaurus serve build --host 127.0.0.1 --port 3000", "serve:intranet": "docusaurus serve build --host 0.0.0.0 --port 3000", "clear": "docusaurus clear" }, "dependencies": { "@docusaurus/core": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@docusaurus/theme-mermaid": "3.10.2", "@easyops-cn/docusaurus-search-local": "0.49.2", "@mermaid-js/layout-elk": "0.1.9", "@mdx-js/react": "3.0.1", "clsx": "2.1.1", "prism-react-renderer": "2.4.0", "react": "18.3.1", "react-dom": "18.3.1" }, "devDependencies": { "typescript": "5.5.4" }, "engines": { "node": ">=18.0" } } # Blueforce docs site (Docusaurus) Static documentation is synced one-way from `../docs/*.md` into `./docs/`; never edit generated `./docs/` directly. ## Local development ```bash cd docs-site npm install npm start # http://127.0.0.1:3000 (loopback only) npm run build npm run serve # local static preview on 127.0.0.1:3000 ``` For an explicitly approved intranet preview, use `npm run start:intranet` or `npm run serve:intranet`; those commands bind `0.0.0.0:3000`. ## Deploy Publish `build/` through the intranet web server. The site has no favicon reference until a maintained favicon asset is added. // Auto-generated sidebar: every synced Markdown file appears without manual entries. // @ts-check /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { docs: [{type: 'autogenerated', dirName: '.'}], }; export default sidebars; #!/usr/bin/env bash # One-way sync: repo docs (../docs/*.md) -> site content (./docs/). # The repo Markdown files are the single source of truth; never edit ./docs by hand. set -euo pipefail SITE_DIR="$(cd "$(dirname "$0")" && pwd)" SRC_DIR="$SITE_DIR/../docs" DST_DIR="$SITE_DIR/docs" mkdir -p "$DST_DIR" # Copy current sources. cp -f "$SRC_DIR"/*.md "$DST_DIR"/ # Remove stale files that no longer exist upstream (keep .gitkeep). for f in "$DST_DIR"/*.md; do base="$(basename "$f")" if [ ! -f "$SRC_DIR/$base" ]; then rm -f "$f" fi done # MDX compatibility: a bare "<" followed by a letter/digit in prose is parsed # as JSX and breaks the build (e.g. "<15 dk"). Escape it as "<" ONLY # outside fenced code blocks and inline code spans, where "<" is safe # (mermaid "
", placeholders like `BF-`). Rendered text is unchanged. python3 - "$DST_DIR" <<'EOF' import re, sys, glob, os dst = sys.argv[1] pat = re.compile(r'<(?=[A-Za-z0-9])') for f in glob.glob(os.path.join(dst, '*.md')): out, in_fence = [], False for line in open(f, encoding='utf-8'): if line.startswith('```'): in_fence = not in_fence out.append(line) continue if in_fence: out.append(line) continue # Split inline code spans; escape only the prose parts. parts = re.split(r'(`[^`]*`)', line) for i in range(0, len(parts), 2): parts[i] = pat.sub('<', parts[i]) out.append(''.join(parts)) open(f, 'w', encoding='utf-8').write(''.join(out)) EOF echo "Synced $(ls "$SRC_DIR"/*.md | wc -l) files into docs-site/docs."
{ "extends": "@docusaurus/tsconfig", "compilerOptions": { "baseUrl": "." } } {"annotations": {"list": []}, "editable": false, "graphTooltip": 0, "panels": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "Devices answering scrapes (up==1) vs silent (up==0).", "fieldConfig": {"defaults": {"mappings": [{"options": {"0": {"text": "OFFLINE"}, "1": {"text": "ONLINE"}}, "type": "value"}], "thresholds": {"mode": "absolute", "steps": [{"color": "red", "value": 0}, {"color": "green", "value": 1}]}}, "overrides": []}, "gridPos": {"h": 4, "w": 8, "x": 0, "y": 0}, "id": 1, "options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "up{job=\"node\",device_id=\"$device\"}", "refId": "A"}], "title": "Online / Offline", "type": "stat"}, {"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "100 - available% across reporting devices.", "fieldConfig": {"defaults": {"max": 100, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "yellow", "value": 70}, {"color": "red", "value": 90}]}, "unit": "percent"}, "overrides": []}, "gridPos": {"h": 4, "w": 8, "x": 8, "y": 0}, "id": 2, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "100 * (1 - node_memory_MemAvailable_bytes{job=\"node\",device_id=\"$device\"} / node_memory_MemTotal_bytes{job=\"node\",device_id=\"$device\"})", "refId": "A"}], "title": "RAM used %", "type": "stat"}, {"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "Average CPU over 5m from node_cpu_seconds_total.", "fieldConfig": {"defaults": {"max": 100, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "yellow", "value": 70}, {"color": "red", "value": 90}]}, "unit": "percent"}, "overrides": []}, "gridPos": {"h": 4, "w": 8, "x": 16, "y": 0}, "id": 3, "options": {"colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "100 * (1 - avg(rate(node_cpu_seconds_total{job=\"node\",device_id=\"$device\",mode=\"idle\"}[5m])) / count(rate(node_cpu_seconds_total{job=\"node\",device_id=\"$device\",mode=\"idle\"}[5m])))", "refId": "A"}], "title": "CPU used %", "type": "stat"}, {"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "Root filesystem usage %.", "fieldConfig": {"defaults": {"max": 100, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "yellow", "value": 75}, {"color": "red", "value": 90}]}, "unit": "percent"}, "overrides": []}, "gridPos": {"h": 6, "w": 12, "x": 0, "y": 4}, "id": 4, "options": {"colorMode": "value", "graphMode": "area", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "100 * (1 - node_filesystem_avail_bytes{job=\"node\",device_id=\"$device\",mountpoint=\"/\"} / node_filesystem_size_bytes{job=\"node\",device_id=\"$device\",mountpoint=\"/\"})", "refId": "A"}], "title": "Disk / used %", "type": "stat"}, {"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "Seconds since the last WireGuard handshake; rising values mean a stale tunnel. Source: textfile metric wireguard_last_handshake_seconds.", "fieldConfig": {"defaults": {"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": 0}, {"color": "yellow", "value": 180}, {"color": "red", "value": 600}]}, "unit": "s"}, "overrides": []}, "gridPos": {"h": 6, "w": 12, "x": 12, "y": 4}, "id": 5, "options": {"colorMode": "value", "graphMode": "area", "justifyMode": "center", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"}, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "time() - wireguard_last_handshake_seconds{job=\"node\",device_id=\"$device\"}", "refId": "A"}], "title": "WireGuard handshake age", "type": "stat"}, {"datasource": {"type": "prometheus", "uid": "prometheus"}, "description": "Running containers (1) vs stopped (0), including MEG. Source: textfile metric docker_container_running.", "fieldConfig": {"defaults": {"mappings": [{"options": {"0": {"text": "STOPPED"}, "1": {"text": "RUNNING"}}, "type": "value"}], "thresholds": {"mode": "absolute", "steps": [{"color": "red", "value": 0}, {"color": "green", "value": 1}]}}, "overrides": []}, "gridPos": {"h": 6, "w": 24, "x": 0, "y": 10}, "id": 6, "targets": [{"datasource": {"type": "prometheus", "uid": "prometheus"}, "expr": "docker_container_running{job=\"node\",device_id=\"$device\"}", "legendFormat": "{{name}}", "refId": "A"}], "title": "Docker containers", "type": "timeseries"}], "refresh": "30s", "schemaVersion": 39, "tags": ["blueforce", "fleet"], "templating": {"list": [{"current": {}, "datasource": {"type": "prometheus", "uid": "prometheus"}, "definition": "label_values(up{job=\"node\"}, device_id)", "includeAll": false, "multi": false, "name": "device", "options": [], "query": {"query": "label_values(up{job=\"node\"}, device_id)", "refId": "PrometheusVariableQueryEditor-VariableQuery"}, "refresh": 1, "regex": "", "type": "query"}]}, "time": {"from": "now-6h", "to": "now"}, "timepicker": {}, "timezone": "", "title": "Blueforce Fleet Overview", "uid": "bf-fleet-overview", "version": 1} # Auto-provision the fleet dashboard from disk (no manual import). apiVersion: 1 providers: - name: blueforce-fleet orgId: 1 folder: Blueforce type: file disableDeletion: false updateIntervalSeconds: 30 allowUiUpdates: false options: path: /etc/grafana/dashboards # Auto-provisioned Prometheus datasource for Grafana OSS. apiVersion: 1 datasources: - name: Prometheus type: prometheus access: proxy url: http://prometheus:9090 isDefault: true editable: false [ { "targets": ["10.7.0.11"], "labels": { "device_id": "BF-12010193", "site": "pilot", "job": "node" } } ] # File-based service discovery for the 700-device fleet. # # Prometheus watches this directory (refresh 30s); adding or removing a # `*.json` file takes effect without a reload or restart. # # Per-device entry template (WireGuard address as target, identity label # contract `device_id="BF-"` on every target): # # [ # { # "targets": ["10.7.0.11"], # "labels": {"device_id": "BF-12010193", "site": "pilot", "job": "node"} # } # ] # # Scale-out: generate one file per site/region (e.g. `site-ankara.json`) # from the Ansible inventory; keep each file under a few thousand targets. # Fleet alert rules. Severity: critical pages the on-call admin (14-MONITORING). # Notification endpoints (SMS/e-mail/webhook) are wired in the central # Grafana Alerting / Alertmanager setup, not in this file. # All expressions assume 60s scrape interval. groups: - name: fleet interval: 60s rules: # Device does not answer scrapes for 5 minutes -> presumed offline. - alert: FleetOffline expr: up{job="node"} == 0 for: 5m labels: severity: critical annotations: summary: "Device {{ $labels.device_id }} is offline" description: "No successful scrape for 5m (instance {{ $labels.instance }}). Check Uptime Kuma last-seen and field power/network." # Root filesystem less than 10% available for 10 minutes. - alert: FleetDiskFull expr: | ( node_filesystem_avail_bytes{job="node", mountpoint="/", fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{job="node", mountpoint="/", fstype!~"tmpfs|overlay"} ) < 0.10 for: 10m labels: severity: critical annotations: summary: "Device {{ $labels.device_id }} disk almost full" description: "Root filesystem <10% available for 10m on {{ $labels.instance }}." # Boot time changed recently -> unexpected reboot (power loss or crash). - alert: FleetUnexpectedReboot expr: time() - node_boot_time_seconds{job="node"} < 15 * 60 for: 5m labels: severity: critical annotations: summary: "Device {{ $labels.device_id }} rebooted unexpectedly" description: "{{ $labels.instance }} booted less than 15m ago. Correlate with power-loss recovery (16-POWER-LOSS)." # unattended-upgrades must stay DISABLED (K-11); 1 means the lock was broken. # Source: textfile metric unattended_upgrade_active. - alert: FleetSecurityUpdatesViolation expr: unattended_upgrade_active{job="node"} == 1 for: 5m labels: severity: critical annotations: summary: "Device {{ $labels.device_id }} has unattended-upgrades enabled" description: "Update policy lock (K-11) violated on {{ $labels.instance }}. Disable unattended-upgrades per 10-UPDATE." # Prometheus configuration for the Blueforce fleet. # Scrape interval 60s keeps cardinality/storage tractable at 700 nodes. # Fleet targets scale via file-based service discovery: add/remove entries # in targets/fleet.json without reloading Prometheus (refresh_interval 30s). global: scrape_interval: 60s scrape_timeout: 10s evaluation_interval: 60s external_labels: fleet: blueforce rule_files: - alerts.yml scrape_configs: # Prometheus self-monitoring. - job_name: prometheus static_configs: - targets: ["localhost:9090"] # Field devices: Node Exporter (:9100) + textfile collector metrics. # Identity label contract: device_id="BF-" on every target. - job_name: node file_sd_configs: - files: ["/etc/prometheus/targets/*.json"] refresh_interval: 30s relabel_configs: - source_labels: [__address__] target_label: __address__ regex: (.+) replacement: ${1}:9100 - source_labels: [device_id] target_label: device_id regex: (.+) replacement: ${1} metric_relabel_configs: # Enforce the device_id label contract; drop series without it. - source_labels: [device_id] regex: .+ action: keep # Central monitoring stack for the Blueforce fleet. # Grafana requires GF_SECURITY_ADMIN_PASSWORD__FILE on first startup; no default credential exists. services: prometheus: image: prom/prometheus:v3.1.0 container_name: bf-prometheus restart: unless-stopped volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro - ./prometheus/targets:/etc/prometheus/targets:ro - prometheus-data:/prometheus command: [--config.file=/etc/prometheus/prometheus.yml, --storage.tsdb.path=/prometheus, --storage.tsdb.retention.time=90d, --web.enable-lifecycle] ports: ["127.0.0.1:9090:9090"] networks: [monitoring] grafana: image: grafana/grafana-oss:11 container_name: bf-grafana restart: unless-stopped depends_on: [prometheus] environment: GF_SECURITY_ADMIN_USER: "${GF_SECURITY_ADMIN_USER:-admin}" GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password secrets: [grafana_admin_password] volumes: - grafana-data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/etc/grafana/dashboards:ro ports: ["127.0.0.1:3001:3000"] networks: [monitoring] uptime-kuma: image: louislam/uptime-kuma:1 container_name: bf-uptime-kuma restart: unless-stopped volumes: [uptime-kuma-data:/app/data] ports: ["127.0.0.1:3002:3001"] networks: [monitoring] secrets: grafana_admin_password: file: "${GRAFANA_ADMIN_PASSWORD_FILE:?Set GRAFANA_ADMIN_PASSWORD_FILE to a root-owned password file}" volumes: prometheus-data: grafana-data: uptime-kuma-data: networks: monitoring: driver: bridge # Central monitoring stack (Prometheus + Grafana OSS + Uptime Kuma) ## Start Create a root-owned password file whose contents are the Grafana first-admin password (no newline is preferred), then start with its absolute path: ```bash install -m 600 -o root -g root /dev/null /etc/blueforce/grafana-admin-password # Write the generated password directly to /etc/blueforce/grafana-admin-password. cd monitoring GRAFANA_ADMIN_PASSWORD_FILE=/etc/blueforce/grafana-admin-password docker compose up -d ``` `GRAFANA_ADMIN_PASSWORD_FILE` is mandatory. Compose mounts that host file as a Docker secret; it is not stored in the repository or printed in the compose environment. The process owner must keep the file root-owned and mode `0600`. Grafana is available at `http://127.0.0.1:3001`; Prometheus at `:9090`; Uptime Kuma at `:3002`. ## Fleet targets Field devices are scraped via file-based service discovery in `prometheus/targets/*.json` (see `prometheus/targets/README.md`). ## Alerts Validate changes with `promtool check rules prometheus/alerts.yml` and `promtool check config prometheus/prometheus.yml`. # RustDesk self-hosted server (hbbs + hbbr) on the central VDS. # OSS edition (AGPL-3.0), unlimited clients, no license cost. # Docs: https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ # # Port contract (VDS firewall must allow these from field-device internet): # 21115/TCP hbbs NAT-type test # 21116/TCP hbbs ID registration / heartbeat # 21116/UDP hbbs TCP hole punching # 21117/TCP hbbr relay traffic # Optional, keep CLOSED unless the web client is used: # 21118/TCP hbbs web client support # 21119/TCP hbbr web client support # Pro-only (not used here): # 21114/TCP Pro web console # # network_mode: host is REQUIRED so hbbs/hbbr see the real client source # IPs (rendezvous + relay decisions break behind Docker NAT). services: hbbs: image: rustdesk/rustdesk-server:1.1.16 container_name: bf-hbbs network_mode: host restart: unless-stopped volumes: - ./data:/root command: hbbs hbbr: image: rustdesk/rustdesk-server:1.1.16 container_name: bf-hbbr network_mode: host restart: unless-stopped volumes: - ./data:/root command: hbbr # RustDesk Self-Hosted Server (hbbs + hbbr) OSS rendezvous + relay server for the Blueforce fleet. Field clients reach the VDS public address directly over the internet, independent of WireGuard: if the tunnel drops, this channel stays up (requires field internet + VDS up). ## Prerequisites - A central VDS with a public DNS name (example: `rustdesk.example.com`). Use your real DNS name; the value below is a placeholder. - TCP/UDP ports 21115-21117 reachable from the internet (see port table in `docker-compose.yml`). Keep 21118/21119 closed unless the web client is used. - Docker Engine + Docker Compose plugin on the VDS. ## Setup ```bash cd rustdesk docker compose up -d # First start generates the keypair in ./data (id_ed25519 + id_ed25519.pub). cat data/id_ed25519.pub # <-- public key; embed in field clients docker logs bf-hbbs # verify hbbs listening docker logs bf-hbbr # verify hbbr listening ``` ## Key distribution (field clients) 1. Read the public key: `cat data/id_ed25519.pub` (single line, no newline issues). 2. Bake into the golden image / deployment script together with the server address: `rustdesk.example.com` (replace with the real VDS DNS name). 3. Client config keys: `host = rustdesk.example.com`, `key = `. 4. Tag each client with its device identity `BF-` so the RustDesk address book matches the Ansible inventory and MeshCentral names. 5. Key rotation: deploy the new key alongside the old one, confirm clients connect, then retire the old key. ## Health monitoring Point Uptime Kuma (see `../monitoring`) at: - TCP 21116 (hbbs heartbeat) - TCP 21117 (hbbr relay) ## Notes - `network_mode: host` is intentional; do not add port mappings. - Data directory `./data` holds the keypair; back it up (see 17-BACKUP-RECOVERY). - Never commit the private key (`data/id_ed25519`) to Git. #!/usr/bin/env bash # # bf-diagnostics - Deep health check for a Blueforce field terminal. # # Detailed version of bf-status: SMART, journal errors, docker ps, # WireGuard state, unattended-upgrades lock, sockets, inodes, textfiles. # Never prints secrets, keys, or passwords (handshake keys are redacted). # # Install: install -m 0755 bf-diagnostics /usr/local/bin/bf-diagnostics # Usage: bf-diagnostics [--help] # set -euo pipefail usage() { cat <<'EOF' Usage: bf-diagnostics [--help] Run deep checks: SMART, journal errors, docker ps, wg show, unattended-upgrades lock state, sockets, inodes, textfile collectors. Options: --help Show this help and exit EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 elif [[ $# -gt 0 ]]; then echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 fi set +e # report, don't abort sec() { printf '\n### %s\n' "$*"; } sec "HOST" hostname; uptime; systemctl get-default 2>/dev/null || echo "target: unknown" sec "SMART" if command -v smartctl >/dev/null 2>&1; then for dev in /dev/sd? /dev/nvme?n1; do [[ -b "${dev}" ]] || continue echo "--- ${dev} ---" smartctl -H -A "${dev}" 2>/dev/null | grep -Ei 'health|reallocated|pending|uncorrect|wear|temperature|power_on' || echo "no SMART attributes parsed" done else echo "smartctl not installed; skipping SMART." fi sec "JOURNAL ERRORS (last 24h, top 20)" if command -v journalctl >/dev/null 2>&1; then journalctl --since "24 hours ago" -p err --no-pager -o short 2>/dev/null | tail -20 || echo "no journal access" else echo "journalctl not available." fi sec "FAILED SYSTEMD UNITS" if command -v systemctl >/dev/null 2>&1; then systemctl --failed --no-pager 2>/dev/null || echo "no failed units" else echo "systemctl not available." fi sec "DOCKER PS" if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null || echo "docker ps failed" echo "--- restart counts ---" docker ps --format '{{.Names}} restarts={{.RestartCount}}' 2>/dev/null || true else echo "docker daemon not reachable." fi sec "WIREGUARD (keys redacted)" if command -v wg >/dev/null 2>&1; then wg show 2>/dev/null | sed -E 's/(private-key|public-key|preshared-key): .*/\1: /' || echo "no WireGuard interfaces" echo "--- latest handshakes ---" wg show all latest-handshakes 2>/dev/null || echo "no handshake data" else echo "wg tool not installed." fi sec "UNATTENDED-UPGRADES LOCK (must be DISABLED)" grep -H . /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null || echo "20auto-upgrades file missing" for t in apt-daily.timer apt-daily-upgrade.timer; do printf '%s: %s\n' "$t" "$(systemctl is-enabled "$t" 2>/dev/null || echo unknown)" done sec "SOCKETS (listening)" ss -ltnp 2>/dev/null || ss -ltn 2>/dev/null || echo "ss not available" sec "DISK + INODES" df -h / /var 2>/dev/null df -i / /var 2>/dev/null | head -5 sec "TEXTFILE COLLECTORS" ls -l /var/lib/node_exporter/textfile/ 2>/dev/null || echo "textfile dir missing" for f in /var/lib/node_exporter/textfile/*.prom; do [[ -f "$f" ]] || continue echo "--- $f ---"; head -10 "$f" done sec "SECURITY UPDATES PENDING" if command -v apt >/dev/null 2>&1; then apt list --upgradable 2>/dev/null | grep -ci security || echo "0 security updates (or apt list unavailable)" else echo "apt not available." fi sec "REBOOT REQUIRED" if [[ -f /var/run/reboot-required ]]; then echo "YES"; cat /var/run/reboot-required.pkgs 2>/dev/null || true else echo "NO"; fi echo echo "Diagnostics complete. For remote triage run: bf-support-bundle" exit 0 #!/usr/bin/env bash # # bf-hardware-inventory.sh - Scan field-terminal hardware and emit JSON + Markdown. # # Covers every field in docs/02-DEVICE-NAMING-AND-INVENTORY.md §9: # Manufacturer/Model/Serial/BIOS/CPU/RAM/Disk/SMART/NIC/MAC/GPU/USB/ # SerialPorts/Kernel/Arch/TPM/Virt/Temps. Missing probes report # "unknown" instead of aborting. Never prints secrets, keys, or passwords. # # Install: install -m 0755 bf-hardware-inventory.sh /usr/local/bin/bf-hardware-inventory.sh # Usage: bf-hardware-inventory.sh [--dealer-id ID] [--out-dir DIR] [--json] [--markdown] [--help] # set -euo pipefail DEALER_ID="" OUT_DIR="" MODE="both" # json | markdown | both usage() { cat <<'EOF' Usage: bf-hardware-inventory.sh [--dealer-id ID] [--out-dir DIR] [--json] [--markdown] [--help] Scan hardware and emit inventory (JSON for machines, Markdown for humans). Options: --dealer-id ID 8-digit dealer number (validates ^[0-9]{8}$ when given) --out-dir DIR Write BF-.json and BF-.md into DIR (requires --dealer-id; default filenames use hostname) --json Print only JSON to stdout --markdown Print only Markdown to stdout --help Show this help and exit Fields (doc 02 §9): Manufacturer, Model, Serial, BIOS, CPU, RAM, Disk, SMART, NIC, MAC, GPU, USB, SerialPorts, Kernel, Arch, TPM, Virt, Temps. Unavailable probes report "unknown". EOF } while [[ $# -gt 0 ]]; do case "$1" in --dealer-id) DEALER_ID="${2:?missing value for --dealer-id}"; shift 2 ;; --out-dir) OUT_DIR="${2:?missing value for --out-dir}"; shift 2 ;; --json) MODE="json"; shift ;; --markdown) MODE="markdown"; shift ;; --help|-h) usage; exit 0 ;; *) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [[ -n "${DEALER_ID}" ]] && ! [[ "${DEALER_ID}" =~ ^[0-9]{8}$ ]]; then echo "ERROR: --dealer-id must match ^[0-9]{8}$ (got: ${DEALER_ID})" >&2 exit 2 fi if [[ -n "${OUT_DIR}" && -z "${DEALER_ID}" ]]; then echo "ERROR: --out-dir requires --dealer-id" >&2 exit 2 fi set +e # probes must not abort the report HOST="$(hostname 2>/dev/null || echo unknown)" if [[ -n "${DEALER_ID}" ]]; then DEVICE_ID="BF-${DEALER_ID}" LOWER_HOST="bf-${DEALER_ID}" else DEVICE_ID="$(echo "${HOST}" | tr '[:lower:]' '[:upper:]')" LOWER_HOST="$(echo "${HOST}" | tr '[:upper:]' '[:lower:]')" fi NOW="$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null || echo unknown)" json_escape() { # stdin -> JSON string body python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])' 2>/dev/null || sed 's/"/\\"/g' | tr -d '\n' } val() { # $1 raw -> "unknown" fallback, no newlines local v="$1" v="$(echo "${v}" | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" [[ -z "${v}" ]] && echo "unknown" || echo "${v}" } # --- Probes (all fields from doc 02) --- MANUFACTURER="$(val "$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || dmidecode -s system-manufacturer 2>/dev/null)")" MODEL="$(val "$(cat /sys/class/dmi/id/product_name 2>/dev/null || dmidecode -s system-product-name 2>/dev/null)")" SERIAL="$(val "$(cat /sys/class/dmi/id/product_serial 2>/dev/null || dmidecode -s system-serial-number 2>/dev/null)")" BIOS_VENDOR="$(val "$(cat /sys/class/dmi/id/bios_vendor 2>/dev/null || dmidecode -s bios-vendor 2>/dev/null)")" BIOS_VERSION="$(val "$(cat /sys/class/dmi/id/bios_version 2>/dev/null || dmidecode -s bios-version 2>/dev/null)")" BIOS_DATE="$(val "$(cat /sys/class/dmi/id/bios_date 2>/dev/null || dmidecode -s bios-release-date 2>/dev/null)")" CPU_MODEL="$(val "$(awk -F': ' '/^model name/ {print $2; exit}' /proc/cpuinfo 2>/dev/null || lscpu 2>/dev/null | awk -F': *' '/Model name/ {print $2; exit}')")" CPU_CORES="$(val "$(nproc 2>/dev/null || grep -c '^processor' /proc/cpuinfo 2>/dev/null)")" ARCH="$(val "$(uname -m 2>/dev/null)")" KERNEL="$(val "$(uname -sr 2>/dev/null)")" MEM_KB="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)" MEMORY_MB="$([ -n "${MEM_KB}" ] && echo $((MEM_KB / 1024)) || echo "unknown")" MEMORY_STR="$([ "${MEMORY_MB}" != "unknown" ] && echo "${MEMORY_MB} MB" || echo "unknown")" DISKS="$(lsblk -d -n -o NAME,SIZE,TYPE,ROTA 2>/dev/null || echo unknown)" GPU="$(val "$(lspci 2>/dev/null | grep -iE 'vga|3d|display' | head -3 | sed 's/^[^:]*: //')")" NIC_LIST="$(val "$(ls /sys/class/net 2>/dev/null | grep -v '^lo$' | tr '\n' ',' | sed 's/,$//')")" MAC_LIST="$(val "$(for n in /sys/class/net/*; do [[ "$(basename "$n")" == lo ]] && continue; cat "$n/address" 2>/dev/null; done | tr '\n' ',' | sed 's/,$//')")" USB_LIST="$(val "$(lsusb 2>/dev/null | head -20 || echo '')")" SERIAL_PORTS="$(val "$(ls /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | tr '\n' ',' | sed 's/,$//')")" TPM="$(val "$( ([ -c /dev/tpm0 ] && echo -n 'present (/dev/tpm0)') ; dmesg 2>/dev/null | grep -im1 'tpm' | head -1 | sed 's/^/ /')")" VIRT="$(val "$(systemd-detect-virt 2>/dev/null || echo none)")" TEMPS="$(val "$( (cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | head -5 | tr '\n' ' '); sensors 2>/dev/null | grep -iE 'core|temp|package' | head -5 | tr '\n' ';')")" SMART_SUMMARY="unknown" if command -v smartctl >/dev/null 2>&1; then SMART_SUMMARY="$(for dev in /dev/sd? /dev/nvme?n1; do [[ -b "${dev}" ]] || continue echo -n "${dev}: " smartctl -H "${dev}" 2>/dev/null | grep -im1 'health\|result' | sed 's/^[[:space:]]*//' | head -1 echo -n '; ' done)" SMART_SUMMARY="$(val "${SMART_SUMMARY}")" else SMART_SUMMARY="smartctl not installed" fi OS_PRETTY="$(val "$(grep -E '^PRETTY_NAME=' /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"')")" E() { printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])' 2>/dev/null || printf '%s' "$1"; } JSON="$(cat < "${OUT_DIR}/BF-${DEALER_ID}.json" printf '%s\n' "${MD}" > "${OUT_DIR}/BF-${DEALER_ID}.md" echo "wrote ${OUT_DIR}/BF-${DEALER_ID}.json ${OUT_DIR}/BF-${DEALER_ID}.md" else printf '%s\n\n%s\n' "${JSON}" "${MD}" fi ;; esac #!/usr/bin/env bash # # bf-status - Quick 16-line health glance for a Blueforce field terminal. # # Output contract: docs/14-MONITORING-AND-HEALTH.md ("bf-status ornek ciktisi"). # Never prints secrets, keys, or passwords. # # Install: install -m 0755 bf-status /usr/local/bin/bf-status # Usage: bf-status [--help] # set -euo pipefail usage() { cat <<'EOF' Usage: bf-status [--help] Print the 16-line BLUEFORCE FIELD TERMINAL health summary. Options: --help Show this help and exit Exit status is always 0; individual failing probes report DOWN/UNKNOWN inline instead of aborting the report. EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 elif [[ $# -gt 0 ]]; then echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 fi # Never abort the report on a failing probe. set +e HOST="$(hostname 2>/dev/null || echo unknown)" DEVICE_ID="$(echo "${HOST}" | tr '[:lower:]' '[:upper:]')" NOW="$(date '+%Y-%m-%d %H:%M %z' 2>/dev/null || echo unknown)" OVERALL="OK" uptime_str() { uptime -p 2>/dev/null | sed 's/^up //' || echo unknown } load_str() { awk '{print $1, $2, $3}' /proc/loadavg 2>/dev/null || echo "unknown" } cpu_pct() { # 1-second sample from /proc/stat. read -r _ a b c d e f g h i j < /proc/stat 2>/dev/null || { echo "?"; return; } idle1=$((d + e)); total1=$((a+b+c+d+e+f+g+h+i+j)) sleep 1 read -r _ a b c d e f g h i j < /proc/stat 2>/dev/null || { echo "?"; return; } idle2=$((d + e)); total2=$((a+b+c+d+e+f+g+h+i+j)) dt=$((total2 - total1)); di=$((idle2 - idle1)) if [[ "${dt}" -le 0 ]]; then echo "?"; else echo $(( (100 * (dt - di)) / dt )); fi } mem_str() { local avail total avail="$(awk '/MemAvailable/ {print $2}' /proc/meminfo 2>/dev/null)" total="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)" if [[ -n "${avail}" && -n "${total}" && "${total}" -gt 0 ]]; then local used=$((total - avail)) local pct=$((100 * used / total)) printf '%s/%sG (%s%%)' \ "$(awk -v v="${used}" 'BEGIN{printf "%.1f", v/1048576}')" \ "$(awk -v v="${total}" 'BEGIN{printf "%.1f", v/1048576}')" \ "${pct}" else echo "unknown" fi } disk_str() { df -BG / 2>/dev/null | awk 'NR==2 {gsub(/G/,"",$2); gsub(/G/,"",$3); gsub(/%/,"",$5); printf "%s/%sG (%s%%)", $3, $2, $5}' || echo "unknown" } svc_state() { # $1 = unit if ! command -v systemctl >/dev/null 2>&1; then echo "UNKNOWN"; return; fi if systemctl is-active --quiet "$1" 2>/dev/null; then echo "OK"; return; fi if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^$1"; then echo "DOWN"; else echo "OFF"; fi } wg_info() { if ! command -v wg >/dev/null 2>&1; then echo "wg0 MISSING (no wg tool)"; return; fi if ! ip link show wg0 >/dev/null 2>&1; then echo "wg0 DOWN (no interface)"; return; fi local hs hs="$(wg show wg0 latest-handshakes 2>/dev/null | awk '{print $2}' | head -1)" if [[ -z "${hs}" || "${hs}" == "0" ]]; then echo "wg0 UP, no handshake yet"; return; fi local now ago now="$(date +%s)"; ago=$((now - hs)) echo "wg0 UP, handshake ${ago}s ago" } net_counters() { local rx=0 tx=0 f for f in /sys/class/net/*/statistics/rx_bytes; do rx=$((rx + $(cat "$f" 2>/dev/null || echo 0))); done for f in /sys/class/net/*/statistics/tx_bytes; do tx=$((tx + $(cat "$f" 2>/dev/null || echo 0))); done echo "rx $(numfmt --to=iec "$rx" 2>/dev/null || echo "$rx") tx $(numfmt --to=iec "$tx" 2>/dev/null || echo "$tx")" } docker_info() { if ! command -v docker >/dev/null 2>&1; then echo "daemon MISSING"; return; fi if docker info >/dev/null 2>&1; then echo "daemon UP"; else echo "daemon DOWN"; fi } meg_info() { # MEG container name convention: meg-* ; report first match. if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then echo "meg:unknown (docker down)"; return; fi local line line="$(docker ps --format '{{.Names}} {{.Status}} {{.RestartCount}}' 2>/dev/null | grep -i '^meg' | head -1)" if [[ -z "${line}" ]]; then echo "meg NOT RUNNING"; return; fi echo "${line}" } meg_last_success() { local f="/var/lib/blueforce/meg_last_success" if [[ -f "${f}" ]]; then local ts now ts="$(cat "${f}" 2>/dev/null | tr -d '[:space:]')" now="$(date +%s)" if [[ "${ts}" =~ ^[0-9]+$ ]]; then echo "UP, last success $((now - ts))s ago"; else echo "UP, last success unknown"; fi else echo "unknown (no timestamp file)" fi } unattended_state() { local masked="no" cfg="unknown" if systemctl is-enabled apt-daily.timer 2>/dev/null | grep -q masked; then masked="yes"; fi if grep -Eq '"0"' /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null; then cfg="0"; fi if [[ "${masked}" == "yes" && "${cfg}" == "0" ]]; then echo "DISABLED (timer masked)"; else echo "CHECK (timer/cfg drift)"; fi } ufw_str() { if ! command -v ufw >/dev/null 2>&1; then echo "ufw not installed"; return; fi if ufw status 2>/dev/null | head -1 | grep -qi active; then echo "active, default DENY in | 22/3389 wg0-only"; else echo "INACTIVE"; fi } gui_str() { local target sessions="0" target="$(systemctl get-default 2>/dev/null || echo unknown)" if ss -ltn 2>/dev/null | grep -q ':3389 '; then sessions="$(ss -tn state established '( dport = :3389 or sport = :3389 )' 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')"; fi if [[ "${target}" == "graphical.target" ]]; then echo "ON (graphical) | rdp sessions: ${sessions}"; else echo "OFF (multi-user) | rdp sessions: ${sessions}"; fi } kuma_str() { local f="/var/lib/blueforce/kuma_last_push" if [[ -f "${f}" ]]; then local ts now ts="$(cat "${f}" 2>/dev/null | tr -d '[:space:]')" now="$(date +%s)" if [[ "${ts}" =~ ^[0-9]+$ ]]; then echo "OK $((now - ts))s ago"; else echo "unknown"; fi else echo "unknown (no push record)" fi } scrape_str() { local f="/var/lib/node_exporter/textfile/last_scrape" if [[ -f "${f}" ]]; then echo "OK ($(cat "${f}" 2>/dev/null | tr -d '[:space:]'))"; else echo "unknown"; fi } rustdesk_str() { if pgrep -x rustdesk >/dev/null 2>&1 || systemctl is-active --quiet rustdesk 2>/dev/null; then echo "online"; else echo "offline"; fi } mesh_str() { if pgrep -i meshcentral >/dev/null 2>&1 || systemctl is-active --quiet meshagent 2>/dev/null; then echo "online"; else echo "offline"; fi } boot_str() { local b b="$(who -b 2>/dev/null | awk '{print $3, $4}' || echo unknown)" echo "${b} (power-loss recovery OK)" } alerts_str() { # Warning heuristic: disk use > 80% counts one warning. local pct pct="$(df / 2>/dev/null | awk 'NR==2 {gsub(/%/,"",$5); print $5}')" if [[ -n "${pct}" && "${pct}" -ge 90 ]] 2>/dev/null; then echo "1 critical, 0 warning (disk ${pct}%)"; elif [[ -n "${pct}" && "${pct}" -ge 80 ]] 2>/dev/null; then echo "0 critical, 1 warning (disk trend)"; else echo "0 critical, 0 warning"; fi } DOCKER_SVC="$(svc_state docker)" WG_SVC="$(svc_state "wg-quick@wg0")" XRDP_SVC="$(svc_state xrdp)" NODE_SVC="$(svc_state node_exporter)" printf '%s %s %s\n' "${DEVICE_ID}" "${NOW}" "${OVERALL}" printf 'uptime: %s | load: %s\n' "$(uptime_str)" "$(load_str)" printf 'cpu: %s%% | mem: %s | disk /: %s\n' "$(cpu_pct)" "$(mem_str)" "$(disk_str)" printf 'net: %s | %s\n' "$(wg_info)" "$(net_counters)" printf 'docker: %s | %s\n' "$(docker_info)" "$(meg_info)" printf 'meg: %s\n' "$(meg_last_success)" printf 'systemd: docker %s | wg-quick@wg0 %s | xrdp %s | node_exporter %s\n' "${DOCKER_SVC}" "${WG_SVC}" "${XRDP_SVC}" "${NODE_SVC}" printf 'unattended-upgrades: %s\n' "$(unattended_state)" printf 'ufw: %s\n' "$(ufw_str)" printf 'gui: %s\n' "$(gui_str)" printf 'kuma push: %s | prometheus scrape: %s\n' "$(kuma_str)" "$(scrape_str)" printf 'rustdesk: %s | meshcentral: %s\n' "$(rustdesk_str)" "$(mesh_str)" printf 'boot: %s\n' "$(boot_str)" printf 'alerts: %s\n' "$(alerts_str)" printf 'next: support bundle: bf-support-bundle\n' #!/usr/bin/env bash # # bf-support-bundle - Collect a secret-free support tarball for a Blueforce field terminal. # # Gathers bf-status + bf-diagnostics output, journal tail, textfile collectors # and basic system facts into a tar.gz. Files matching secret patterns # (*.key, *secret*, password material, private keys) are NEVER included, # and key-like values in captured command output are redacted. # A manifest.txt inside the bundle lists every file + collection time. # # Output contract: docs/14-MONITORING-AND-HEALTH.md ("bf-support-bundle ... # secret ... ASLA pakete girmez"). # # Install: install -m 0755 bf-support-bundle /usr/local/bin/bf-support-bundle # Usage: bf-support-bundle [--out-dir DIR] [--help] # set -euo pipefail OUT_DIR="/var/tmp" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <<'EOF' Usage: bf-support-bundle [--out-dir DIR] [--help] Collect a secret-free support bundle (tar.gz with manifest.txt). Options: --out-dir DIR Directory for the tarball (default: /var/tmp) --help Show this help and exit Excluded (never packaged): *.key, *secret*, *password*, *passwd*, *private*, *id_rsa*, *id_ed25519*, *.pem, WireGuard private keys, RustDesk passwords, SSH private keys. Manifest: manifest.txt inside the tarball lists each file, byte size, collection timestamp, and the exclusion patterns applied. EOF } while [[ $# -gt 0 ]]; do case "$1" in --out-dir) OUT_DIR="${2:?missing value for --out-dir}"; shift 2 ;; --help|-h) usage; exit 0 ;; *) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done HOST="$(hostname 2>/dev/null || echo unknown)" STAMP="$(date '+%Y%m%d-%H%M%S' 2>/dev/null || echo unknown)" WORK="$(mktemp -d)" trap 'rm -rf "${WORK}"' EXIT BUNDLE_NAME="blueforce-support-${HOST}-${STAMP}" STAGE="${WORK}/${BUNDLE_NAME}" mkdir -p "${STAGE}" log() { echo "[bf-support-bundle] $*" >&2; } # Exclude patterns: filename-level, applied to every copy step and tar. EXCLUDE_PATTERNS=( '*.key' '*secret*' '*password*' '*passwd*' '*private*' '*id_rsa*' '*id_ed25519*' '*.pem' '*.p12' '*.pfx' ) is_excluded() { # $1 = basename local base="$1" pat for pat in "${EXCLUDE_PATTERNS[@]}"; do # shellcheck disable=SC2053 [[ "${base}" == ${pat} ]] && return 0 done return 1 } redact() { # stdin -> stdout: mask key-like values sed -E \ -e 's/(private-key|public-key|preshared-key)([[:space:]]*[:=][[:space:]]*).*/\1\2/I' \ -e 's/(password|passwd|secret|token)([[:space:]]*[:=][[:space:]]*)[^[:space:]]+/\1\2/I' } collect_cmd() { # $1 = outfile, $@ = command local out="$1"; shift { "$@" 2>&1 || echo "(command failed: $*)"; } | redact > "${out}" || true } log "collecting into ${STAGE}" collect_cmd "${STAGE}/bf-status.txt" "${SCRIPT_DIR}/bf-status" || true if [[ -x "${SCRIPT_DIR}/bf-diagnostics" ]]; then collect_cmd "${STAGE}/bf-diagnostics.txt" "${SCRIPT_DIR}/bf-diagnostics" || true fi collect_cmd "${STAGE}/system.txt" bash -c 'hostname; uptime; uname -a; systemctl get-default 2>/dev/null || echo "target: unknown"' collect_cmd "${STAGE}/disk-df.txt" bash -c 'df -h / /var 2>/dev/null; echo ---; df -i / /var 2>/dev/null' collect_cmd "${STAGE}/sockets.txt" bash -c 'ss -ltnp 2>/dev/null || ss -ltn 2>/dev/null || echo "ss not available"' collect_cmd "${STAGE}/failed-units.txt" bash -c 'systemctl --failed --no-pager 2>/dev/null || echo "no failed units"' if command -v journalctl >/dev/null 2>&1; then journalctl --since "24 hours ago" -p err --no-pager -o short 2>/dev/null | redact | tail -200 > "${STAGE}/journal-err-24h.txt" || echo "no journal access" > "${STAGE}/journal-err-24h.txt" else echo "journalctl not available." > "${STAGE}/journal-err-24h.txt" fi if command -v wg >/dev/null 2>&1; then { wg show 2>/dev/null | redact || echo "no WireGuard interfaces"; echo "--- latest handshakes ---"; wg show all latest-handshakes 2>/dev/null || echo "no handshake data"; } > "${STAGE}/wireguard.txt" || true else echo "wg tool not installed." > "${STAGE}/wireguard.txt" fi # Textfile collectors: copy *.prom only, never anything matching excludes. mkdir -p "${STAGE}/textfile" if [[ -d /var/lib/node_exporter/textfile ]]; then while IFS= read -r -d '' f; do base="$(basename "${f}")" if is_excluded "${base}"; then log "skipping excluded file: ${base}" continue fi cp "${f}" "${STAGE}/textfile/${base}" 2>/dev/null || true done < <(find /var/lib/node_exporter/textfile -maxdepth 1 -name '*.prom' -type f -print0 2>/dev/null || true) if [[ -z "$(ls -A "${STAGE}/textfile" 2>/dev/null)" ]]; then echo "textfile dir empty." > "${STAGE}/textfile/README.txt" fi else echo "textfile dir missing." > "${STAGE}/textfile/README.txt" fi # Manifest: file list + sizes + timestamp + exclusion rules. { echo "blueforce support bundle manifest" echo "host: ${HOST}" echo "collected_at: $(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo unknown)" echo "generator: bf-support-bundle" echo "excluded_patterns: ${EXCLUDE_PATTERNS[*]}" echo "note: secrets, private keys and passwords are never included; key-like values redacted." echo "" echo "files:" ( cd "${STAGE}" && find . -type f -printf '%p %s bytes\n' 2>/dev/null | sort ) || ( cd "${STAGE}" && find . -type f -exec wc -c {} + | sort ) } > "${STAGE}/manifest.txt" mkdir -p "${OUT_DIR}" TARBALL="${OUT_DIR}/${BUNDLE_NAME}.tar.gz" tar --sort=name --mtime='UTC 2020-01-01' \ --exclude='*.key' --exclude='*secret*' --exclude='*password*' \ --exclude='*passwd*' --exclude='*private*' \ --exclude='*id_rsa*' --exclude='*id_ed25519*' --exclude='*.pem' \ -czf "${TARBALL}" -C "${WORK}" "${BUNDLE_NAME}" log "wrote ${TARBALL}" echo "${TARBALL}" #!/usr/bin/env bash # Pre-flight checks: root, Ubuntu 26.04, RAM/disk, internet. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="01-precheck" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [01-precheck] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi MIN_RAM_MB=2048 MIN_DISK_MB=20480 fail() { log "FAIL: $1"; echo "FAIL: $1" >&2; exit 1; } run_checks() { [[ "$(id -u)" -eq 0 ]] || fail "must run as root" if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release [[ "${VERSION_ID:-}" == 26.04* ]] || fail "unsupported OS: ${NAME:-unknown} ${VERSION_ID:-unknown} (need Ubuntu 26.04)" else fail "/etc/os-release not found" fi local ram_mb disk_mb ram_mb=$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo) [[ "$ram_mb" -ge "$MIN_RAM_MB" ]] || fail "RAM too low: ${ram_mb}MB (need >= ${MIN_RAM_MB}MB)" disk_mb=$(df -m / --output=avail | tail -1 | tr -d ' ') [[ "$disk_mb" -ge "$MIN_DISK_MB" ]] || fail "disk too low: ${disk_mb}MB free on / (need >= ${MIN_DISK_MB}MB)" if ! getent hosts archive.ubuntu.com >/dev/null 2>&1 && ! ping -c1 -W5 1.1.1.1 >/dev/null 2>&1; then fail "no internet connectivity (DNS and ping both failed)" fi log "OK: precheck passed (Ubuntu ${VERSION_ID}, RAM ${ram_mb}MB, disk ${disk_mb}MB free)" } run_checks #!/usr/bin/env bash # Base APT sources, disable unattended-upgrades, set timezone. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="02-system" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [02-system] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi TIMEZONE="${TIMEZONE:-Europe/Istanbul}" BASE_PKGS="curl wget ca-certificates gnupg lsb-release" check_state() { local ok=1 for p in $BASE_PKGS; do dpkg -s "$p" >/dev/null 2>&1 || { echo "missing package: $p"; ok=0; }; done grep -q '^APT::Periodic::Update-Package-Lists "0"' /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null || { echo "20auto-upgrades not locked"; ok=0; } for t in apt-daily.service apt-daily.timer apt-daily-upgrade.timer; do systemctl is-enabled "$t" 2>/dev/null | grep -q "masked" || { echo "timer not masked: $t"; ok=0; } done [[ "$(cat /etc/timezone 2>/dev/null || timedatectl show -p Timezone --value 2>/dev/null)" == "$TIMEZONE" ]] || { echo "timezone not $TIMEZONE"; ok=0; } return $((1 - ok)) } if [[ "$CHECK_MODE" -eq 1 ]]; then if check_state; then log "OK(check): system baseline matches"; else log "FAIL(check): system baseline drift"; exit 1; fi exit 0 fi export DEBIAN_FRONTEND=noninteractive apt-get update # shellcheck disable=SC2086 apt-get install -y $BASE_PKGS # Lock automatic updates OFF (fleet policy: no unapproved updates). printf 'APT::Periodic::Update-Package-Lists "0";\nAPT::Periodic::Unattended-Upgrade "0";\n' > /tmp/20auto-upgrades.bf mv /tmp/20auto-upgrades.bf /etc/apt/apt.conf.d/20auto-upgrades for t in apt-daily.service apt-daily.timer apt-daily-upgrade.timer apt-daily-upgrade.service; do systemctl mask "$t" >/dev/null 2>&1 || true done if command -v timedatectl >/dev/null 2>&1; then timedatectl set-timezone "$TIMEZONE" || echo "$TIMEZONE" > /etc/timezone else echo "$TIMEZONE" > /etc/timezone fi log "OK: base packages installed, unattended-upgrades disabled, timezone $TIMEZONE" #!/usr/bin/env bash # Blueforce maintenance user, restricted sudoers, mandatory administrator key. set -euo pipefail MODULE="03-user" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 SUDOERS_FILE="/etc/sudoers.d/blueforce" AUTHORIZED_KEYS="/home/blueforce/.ssh/authorized_keys" BF_ADMIN_PUBLIC_KEY="${BF_ADMIN_PUBLIC_KEY:-}" BF_ADMIN_PUBLIC_KEY_FILE="${BF_ADMIN_PUBLIC_KEY_FILE:-}" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [03-user] $1" | tee -a "$LOG_FILE"; } valid_key_file() { [[ -s "$AUTHORIZED_KEYS" ]] && ssh-keygen -l -f "$AUTHORIZED_KEYS" >/dev/null 2>&1; } load_admin_key() { if [[ -n "$BF_ADMIN_PUBLIC_KEY" && -n "$BF_ADMIN_PUBLIC_KEY_FILE" ]]; then log "FAIL: set only one of BF_ADMIN_PUBLIC_KEY or BF_ADMIN_PUBLIC_KEY_FILE"; return 1 elif [[ -n "$BF_ADMIN_PUBLIC_KEY" ]]; then printf '%s\n' "$BF_ADMIN_PUBLIC_KEY" elif [[ -n "$BF_ADMIN_PUBLIC_KEY_FILE" && -r "$BF_ADMIN_PUBLIC_KEY_FILE" ]]; then cat "$BF_ADMIN_PUBLIC_KEY_FILE" else log "FAIL: BF_ADMIN_PUBLIC_KEY or BF_ADMIN_PUBLIC_KEY_FILE is required before SSH hardening"; return 1 fi } if [[ "$CHECK_MODE" -eq 1 ]]; then id blueforce >/dev/null 2>&1 || { log "FAIL(check): user blueforce missing"; exit 1; } [[ -f "$SUDOERS_FILE" ]] && visudo -cf "$SUDOERS_FILE" >/dev/null 2>&1 || { log "FAIL(check): sudoers invalid"; exit 1; } valid_key_file || { log "FAIL(check): authorized_keys is empty or invalid"; exit 1; } log "OK(check): user, sudoers, and a valid administrator key are present"; exit 0 fi key_data="$(load_admin_key)" || exit 1 [[ -n "${key_data//[[:space:]]/}" ]] || { log "FAIL: administrator key is empty"; exit 1; } tmp_key="$(mktemp)"; trap 'rm -f "$tmp_key"' EXIT printf '%s\n' "$key_data" > "$tmp_key" ssh-keygen -l -f "$tmp_key" >/dev/null 2>&1 || { log "FAIL: supplied administrator public key is invalid"; exit 1; } if ! id blueforce >/dev/null 2>&1; then useradd -m -s /bin/bash blueforce; fi passwd -l blueforce >/dev/null 2>&1 || true install -d -m 700 -o blueforce -g blueforce /home/blueforce/.ssh install -m 600 -o blueforce -g blueforce "$tmp_key" "$AUTHORIZED_KEYS" valid_key_file || { log "FAIL: atomic authorized_keys install did not validate"; exit 1; } cat > /tmp/blueforce.sudoers <<'EOF' # Managed by blueforce-installer (03-user). Local edits will be overwritten. blueforce ALL=(root) NOPASSWD: /usr/local/bin/bf-status, /usr/local/bin/bf-gui-on, /usr/local/bin/bf-gui-off, /bin/systemctl status *, /bin/systemctl is-active *, /bin/systemctl is-enabled *, /usr/bin/docker ps, /usr/bin/docker logs * EOF visudo -cf /tmp/blueforce.sudoers install -m 440 /tmp/blueforce.sudoers "$SUDOERS_FILE" log "OK: user blueforce has a validated administrator key and restricted sudoers" #!/usr/bin/env bash # Set hostname bf- from validated dealer number. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="04-hostname" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [04-hostname] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi [[ -n "$DEALER_ID" ]] || { log "FAIL: DEALER_ID env is empty"; echo "FAIL: DEALER_ID env is empty" >&2; exit 1; } [[ "$DEALER_ID" =~ ^[0-9]{8}$ ]] || { log "FAIL: bad DEALER_ID format: $DEALER_ID"; echo "FAIL: bad DEALER_ID" >&2; exit 1; } TARGET="bf-${DEALER_ID}" if [[ "$CHECK_MODE" -eq 1 ]]; then [[ "$(hostname)" == "$TARGET" ]] || { log "FAIL(check): hostname is $(hostname), want $TARGET"; exit 1; } log "OK(check): hostname is $TARGET" exit 0 fi if [[ "$(hostname)" == "$TARGET" ]]; then log "hostname already $TARGET, keeping" else hostnamectl set-hostname "$TARGET" if grep -q "127.0.1.1" /etc/hosts; then sed -i "s/^127.0.1.1.*/127.0.1.1\t$TARGET/" /etc/hosts else echo -e "127.0.1.1\t$TARGET" >> /etc/hosts fi log "hostname set to $TARGET" fi mkdir -p "$STATE_DIR" echo "$DEALER_ID" > "$STATE_DIR/dealer-id" echo "bf-${DEALER_ID}" > "$STATE_DIR/hostname" echo "BF-${DEALER_ID}" > "$STATE_DIR/device-id" log "OK: identity files written (BF-${DEALER_ID} / bf-${DEALER_ID})" #!/usr/bin/env bash # Wait for NetworkManager, verify DNS and connectivity. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="05-network" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [05-network] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi WAIT_SECS="${NM_WAIT_SECS:-90}" if [[ "$CHECK_MODE" -eq 1 ]]; then getent hosts archive.ubuntu.com >/dev/null 2>&1 || { log "FAIL(check): DNS resolution broken"; exit 1; } log "OK(check): DNS resolves" exit 0 fi elapsed=0 while [[ "$elapsed" -lt "$WAIT_SECS" ]]; do if systemctl is-active --quiet NetworkManager 2>/dev/null || systemctl is-active --quiet systemd-networkd 2>/dev/null; then break fi sleep 5; elapsed=$((elapsed + 5)) done systemctl is-active --quiet NetworkManager 2>/dev/null || systemctl is-active --quiet systemd-networkd 2>/dev/null || { log "FAIL: no network manager active after ${WAIT_SECS}s"; exit 1; } getent hosts archive.ubuntu.com >/dev/null 2>&1 || { log "FAIL: DNS resolution failed"; exit 1; } [[ -f /etc/resolv.conf ]] || { log "FAIL: /etc/resolv.conf missing"; exit 1; } log "OK: network manager active, DNS resolves" #!/usr/bin/env bash # WireGuard per-device configuration; refuse placeholders or shared defaults. # Idempotent: safe to re-run. Supports --check for read-only verification. set -euo pipefail MODULE="06-wireguard" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 WG_DIR="/etc/wireguard" WG_CONF="$WG_DIR/wg0.conf" WG_ADDRESS="${WG_ADDRESS:-}" WG_ENDPOINT="${WG_ENDPOINT:-}" WG_PEER_PUBKEY="${WG_PEER_PUBKEY:-}" REQUIRE_WIREGUARD="${REQUIRE_WIREGUARD:-true}" BF_WG_HANDSHAKE_MAX_AGE="${BF_WG_HANDSHAKE_MAX_AGE:-180}" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [06-wireguard] $1" | tee -a "$LOG_FILE"; } valid_address() { local address="$1" ip prefix octet [[ "$address" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[12][0-9]|3[0-2])$ ]] && [[ "$address" != "10.88.0.0/24" ]] || return 1 ip="${address%/*}"; prefix="${address#*/}" IFS=. read -r -a octets <<< "$ip" for octet in "${octets[@]}"; do ((10#$octet <= 255)) || return 1; done ((10#$prefix <= 32)) } valid_endpoint() { local host port [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*:[1-9][0-9]{0,4}$ ]] && [[ "$1" != "vpn.example.com:51820" ]] || return 1 host="${1%:*}"; port="${1##*:}" [[ -n "$host" ]] && ((10#$port <= 65535)) } valid_pubkey() { [[ "$1" =~ ^[A-Za-z0-9+/]{43}=$ ]] && [[ "$1" != "REPLACE_WITH_SERVER_PUBLIC_KEY" ]]; } valid_handshake_age() { [[ "$BF_WG_HANDSHAKE_MAX_AGE" =~ ^[0-9]+$ ]] && ((10#$BF_WG_HANDSHAKE_MAX_AGE > 0)); } config_is_complete() { [[ -f "$WG_CONF" ]] && ! grep -qE '10\.88\.0\.0/24|vpn\.example\.com:51820|REPLACE_WITH_SERVER_PUBLIC_KEY' "$WG_CONF" && grep -qE '^Address = ([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[12][0-9]|3[0-2])$' "$WG_CONF" && grep -qE '^Endpoint = [A-Za-z0-9][A-Za-z0-9.-]*:[1-9][0-9]{0,4}$' "$WG_CONF" && grep -qE '^PublicKey = [A-Za-z0-9+/]{43}=$' "$WG_CONF" } handshake_is_fresh() { local now valid_handshake_age || return 1 now="$(date +%s)" wg show wg0 latest-handshakes 2>/dev/null | awk -v now="$now" -v max_age="$BF_WG_HANDSHAKE_MAX_AGE" '$2 > 0 && now >= $2 && now - $2 <= max_age { found=1 } END { exit(found ? 0 : 1) }' } if [[ "$CHECK_MODE" -eq 1 ]]; then [[ "$REQUIRE_WIREGUARD" == "true" || "$REQUIRE_WIREGUARD" == "false" ]] || { log "FAIL(check): REQUIRE_WIREGUARD must be true or false"; exit 1; } [[ -f "$WG_DIR/privatekey" && -f "$WG_DIR/publickey" ]] || { log "FAIL(check): wireguard keys missing"; exit 1; } config_is_complete || { log "FAIL(check): wg0.conf is missing or contains incomplete/placeholder peer data"; exit 1; } systemctl is-enabled --quiet wg-quick@wg0 || { log "FAIL(check): wg-quick@wg0 not enabled"; exit 1; } systemctl is-active --quiet wg-quick@wg0 || { log "FAIL(check): wg-quick@wg0 not active"; exit 1; } wg show interfaces | tr ' ' '\n' | grep -qx wg0 || { log "FAIL(check): wg0 interface missing"; exit 1; } if [[ "$REQUIRE_WIREGUARD" == "true" ]]; then handshake_is_fresh || { log "FAIL(check): no WireGuard peer handshake within ${BF_WG_HANDSHAKE_MAX_AGE}s"; exit 1; } fi log "OK(check): wireguard config is complete, wg0 is active, and required handshake is fresh" exit 0 fi valid_address "$WG_ADDRESS" || { log "FAIL: WG_ADDRESS must be a unique device CIDR, not a default or placeholder"; exit 1; } valid_endpoint "$WG_ENDPOINT" || { log "FAIL: WG_ENDPOINT must be a non-placeholder host:port"; exit 1; } valid_pubkey "$WG_PEER_PUBKEY" || { log "FAIL: WG_PEER_PUBKEY must be a valid non-placeholder WireGuard public key"; exit 1; } [[ "$REQUIRE_WIREGUARD" == "true" || "$REQUIRE_WIREGUARD" == "false" ]] || { log "FAIL: REQUIRE_WIREGUARD must be true or false"; exit 1; } valid_handshake_age || { log "FAIL: BF_WG_HANDSHAKE_MAX_AGE must be a positive number of seconds"; exit 1; } command -v wg >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y wireguard; } install -d -m 700 "$WG_DIR" if [[ ! -f "$WG_DIR/privatekey" ]]; then umask 077 wg genkey | tee "$WG_DIR/privatekey" | wg pubkey > "$WG_DIR/publickey" chmod 600 "$WG_DIR/privatekey" "$WG_DIR/publickey" log "wireguard keypair generated" fi PRIVKEY="$(<"$WG_DIR/privatekey")" PEER_NAME="${BF_HOSTNAME:-bf-${DEALER_ID:-unknown}}" tmp_conf="$(mktemp "$WG_DIR/wg0.conf.XXXXXX")" cat > "$tmp_conf" < #!/usr/bin/env bash # Harden sshd only after a validated Blueforce administrator key is installed. set -euo pipefail MODULE="07-ssh" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 DROPIN="/etc/ssh/sshd_config.d/99-blueforce.conf" AUTHORIZED_KEYS="/home/blueforce/.ssh/authorized_keys" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [07-ssh] $1" | tee -a "$LOG_FILE"; } key_ready() { [[ -s "$AUTHORIZED_KEYS" ]] && ssh-keygen -l -f "$AUTHORIZED_KEYS" >/dev/null 2>&1; } read -r -d '' WANT <<'EOF' || true # Managed by blueforce-installer (07-ssh). Local edits will be overwritten. PermitRootLogin no PasswordAuthentication no PubkeyAuthentication yes ChallengeResponseAuthentication no UsePAM yes X11Forwarding no ClientAliveInterval 300 ClientAliveCountMax 2 MaxAuthTries 3 AllowUsers blueforce EOF if [[ "$CHECK_MODE" -eq 1 ]]; then [[ -f "$DROPIN" ]] || { log "FAIL(check): sshd drop-in missing"; exit 1; } key_ready || { log "FAIL(check): no valid non-empty blueforce authorized_keys"; exit 1; } grep -qx 'AllowUsers blueforce' "$DROPIN" || { log "FAIL(check): AllowUsers policy is incorrect"; exit 1; } sshd -t 2>/dev/null || { log "FAIL(check): sshd config test failed"; exit 1; } log "OK(check): hardened SSH permits only blueforce with a valid key"; exit 0 fi export DEBIAN_FRONTEND=noninteractive command -v sshd >/dev/null 2>&1 || apt-get install -y openssh-server key_ready || { log "FAIL: valid non-empty /home/blueforce/.ssh/authorized_keys is required before SSH hardening"; exit 1; } printf '%s\n' "$WANT" > /tmp/99-blueforce.conf.bf install -m 644 /tmp/99-blueforce.conf.bf "$DROPIN" sshd -t || { log "FAIL: sshd -t rejected the new config"; exit 1; } if systemctl list-unit-files | grep -q '^sshd\.service'; then SVC=sshd; else SVC=ssh; fi systemctl enable "$SVC" >/dev/null 2>&1 || true systemctl reload-or-restart "$SVC" log "OK: hardened SSH installed after valid key verification; AllowUsers=blueforce" #!/usr/bin/env bash # xRDP + GNOME session, terminal boot default, multi-user. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="08-rdp" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [08-rdp] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi if [[ "$CHECK_MODE" -eq 1 ]]; then dpkg -s xrdp >/dev/null 2>&1 || { log "FAIL(check): xrdp not installed"; exit 1; } systemctl is-enabled --quiet xrdp 2>/dev/null || { log "FAIL(check): xrdp not enabled"; exit 1; } [[ "$(systemctl get-default)" == "multi-user.target" ]] || { log "FAIL(check): default target is not multi-user"; exit 1; } log "OK(check): xrdp installed+enabled, terminal boot default" exit 0 fi export DEBIAN_FRONTEND=noninteractive dpkg -s xrdp >/dev/null 2>&1 || apt-get install -y xrdp dpkg -s gnome-session >/dev/null 2>&1 || apt-get install -y gnome-session # Default GNOME session for RDP logins (per-user override still wins). if [[ ! -f /etc/skel/.xsession ]]; then echo "gnome-session" > /etc/skel/.xsession fi if id blueforce >/dev/null 2>&1 && [[ ! -f /home/blueforce/.xsession ]]; then echo "gnome-session" > /home/blueforce/.xsession chown blueforce:blueforce /home/blueforce/.xsession fi systemctl enable xrdp xrdp-sesman >/dev/null 2>&1 || systemctl enable xrdp systemctl set-default multi-user.target log "OK: xrdp ready, gnome-session default, boot target multi-user" #!/usr/bin/env bash # RustDesk is a required remote channel: only a pinned supplied package is accepted. set -euo pipefail MODULE="09-rustdesk" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 RUSTDESK_SERVER="${RUSTDESK_SERVER:-}" RUSTDESK_DEB="${RUSTDESK_DEB:-}" SYSTEM_CONFIG="/etc/rustdesk/RustDesk2.toml" USER_CONFIG="/home/blueforce/.config/RustDesk/RustDesk2.toml" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [09-rustdesk] $1" | tee -a "$LOG_FILE"; } valid_server() { local endpoint="$1" host port [[ -n "$endpoint" ]] || return 1 [[ "$endpoint" != *example* && "$endpoint" != *invalid* && "$endpoint" != *REPLACE* && "$endpoint" != *replace* && "$endpoint" != *'<'* && "$endpoint" != *'>'* && "$endpoint" != localhost* ]] || return 1 [[ "$endpoint" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*(:[1-9][0-9]{0,4})?$ ]] || return 1 host="${endpoint%%:*}" [[ -n "$host" ]] || return 1 if [[ "$endpoint" == *:* ]]; then port="${endpoint##*:}" ((10#$port <= 65535)) || return 1 fi } config_endpoint() { sed -n "s/^rendezvous-server = '\([^']*\)'$/\1/p" "$SYSTEM_CONFIG" 2>/dev/null | head -n 1 } configured() { local endpoint endpoint="$(config_endpoint)" valid_server "$endpoint" || return 1 grep -Fqx "rendezvous-server = '${endpoint}'" "$SYSTEM_CONFIG" >/dev/null 2>&1 && grep -Fqx "relay-server = '${endpoint}'" "$SYSTEM_CONFIG" >/dev/null 2>&1 && grep -Fqx "rendezvous-server = '${endpoint}'" "$USER_CONFIG" >/dev/null 2>&1 && grep -Fqx "relay-server = '${endpoint}'" "$USER_CONFIG" >/dev/null 2>&1 } if [[ "$CHECK_MODE" -eq 1 ]]; then dpkg -s rustdesk >/dev/null 2>&1 || { log "FAIL(check): rustdesk remote channel is not installed"; exit 1; } configured || { log "FAIL(check): RustDesk client config is missing, inconsistent, or uses a placeholder/non-self-hosted endpoint"; exit 1; } rustdesk --get-id 2>/dev/null | grep -qE '^[0-9]+$' || { log "FAIL(check): RustDesk client cannot provide a device ID"; exit 1; } log "OK(check): RustDesk remote channel is installed with a self-hosted client configuration" exit 0 fi [[ -n "$RUSTDESK_DEB" && -f "$RUSTDESK_DEB" ]] || { log "FAIL: RUSTDESK_DEB must name a supplied pinned RustDesk .deb; apt sources are not used"; exit 1; } valid_server "$RUSTDESK_SERVER" || { log "FAIL: RUSTDESK_SERVER must be a non-placeholder self-hosted host or host:port"; exit 1; } export DEBIAN_FRONTEND=noninteractive dpkg -s rustdesk >/dev/null 2>&1 || { dpkg -i "$RUSTDESK_DEB" || apt-get install -f -y; } for cfgdir in /etc/rustdesk /home/blueforce/.config/RustDesk; do install -d -m 700 "$cfgdir" cat > "$cfgdir/RustDesk2.toml" </dev/null || true configured || { log "FAIL: RustDesk client configuration did not persist"; exit 1; } install -d -m 700 "$STATE_DIR" rustdesk --get-id 2>/dev/null | head -1 > "$STATE_DIR/rustdesk-id" || true [[ -s "$STATE_DIR/rustdesk-id" ]] || { log "FAIL: RustDesk client did not provide a device ID"; exit 1; } log "OK: RustDesk pinned package installed and self-hosted channel configured" #!/usr/bin/env bash # Docker CE + daemon.json with log rotation, service enabled. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="10-docker" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [10-docker] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi DAEMON_JSON="/etc/docker/daemon.json" WANT_JSON='{ "log-driver": "json-file", "log-opts": {"max-size": "10m", "max-file": "3"}, "live-restore": true }' if [[ "$CHECK_MODE" -eq 1 ]]; then command -v docker >/dev/null 2>&1 || { log "FAIL(check): docker missing"; exit 1; } systemctl is-enabled --quiet docker 2>/dev/null || { log "FAIL(check): docker not enabled"; exit 1; } [[ -f "$DAEMON_JSON" ]] || { log "FAIL(check): daemon.json missing"; exit 1; } log "OK(check): docker installed, enabled, daemon.json present" exit 0 fi if ! command -v docker >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then install -d -m 0755 /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg # shellcheck disable=SC1091 . /etc/os-release echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list apt-get update fi apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || apt-get install -y docker.io fi printf '%s\n' "$WANT_JSON" > /tmp/daemon.json.bf mkdir -p /etc/docker mv /tmp/daemon.json.bf "$DAEMON_JSON" systemctl enable --now docker systemctl is-active --quiet docker || { log "FAIL: docker installed but service not running"; exit 1; } log "OK: docker ready with log rotation (10m x3) and live-restore" #!/usr/bin/env bash # MEG placeholder: install vendor .deb if present, else SKIP + acceptance checklist. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="11-meg" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [11-meg] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi MEG_DEB="${MEG_DEB:-$(ls /opt/blueforce/debs/meg*.deb /opt/blueforce/meg*.deb 2>/dev/null | head -1)}" MEG_CONTAINER="${MEG_CONTAINER:-meg}" acceptance_checklist() { local fails=0 # Gate 2: pinned version, never latest. local img img=$(docker inspect --format '{{.Config.Image}}' "$MEG_CONTAINER" 2>/dev/null || echo "") if [[ -z "$img" ]]; then echo "CHECK container-running: SKIP (no container $MEG_CONTAINER yet)"; else echo "CHECK container-running: PASS ($img)"; fi if [[ "$img" == *":latest" ]]; then echo "CHECK pinned-tag: FAIL (latest forbidden)"; fails=$((fails+1)); else echo "CHECK pinned-tag: PASS"; fi # Gate 4: restart policy unless-stopped. if [[ -n "$img" ]]; then local pol pol=$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$MEG_CONTAINER" 2>/dev/null) [[ "$pol" == "unless-stopped" ]] && echo "CHECK restart-policy: PASS" || { echo "CHECK restart-policy: FAIL ($pol)"; fails=$((fails+1)); } fi # Gate 5: no 0.0.0.0 port bind to the outside. if ss -tlnp 2>/dev/null | grep -q "0.0.0.0"; then echo "CHECK port-bind: WARN (external bind present — review)"; else echo "CHECK port-bind: PASS"; fi return "$fails" } if [[ "$CHECK_MODE" -eq 1 ]]; then if [[ -z "${MEG_DEB:-}" || ! -f "$MEG_DEB" ]] && ! dpkg -s meg >/dev/null 2>&1 && ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$MEG_CONTAINER"; then log "OK(check): MEG not supplied yet (SKIP state)" exit 0 fi acceptance_checklist || { log "FAIL(check): MEG acceptance checklist failed"; exit 1; } log "OK(check): MEG acceptance checklist passed" exit 0 fi if [[ "${MEG_SKIP:-0}" == "1" ]]; then log "SKIP: MEG_SKIP=1"; exit 3; fi if ! dpkg -s meg >/dev/null 2>&1 && ! docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$MEG_CONTAINER"; then if [[ -n "${MEG_DEB:-}" && -f "$MEG_DEB" ]]; then export DEBIAN_FRONTEND=noninteractive dpkg -i "$MEG_DEB" || apt-get install -f -y log "MEG package installed from $MEG_DEB" else log "SKIP: no MEG .deb supplied — vendor package pending, continuing" exit 3 fi else log "MEG already present, keeping" fi if ! acceptance_checklist; then log "FAIL: MEG acceptance checklist failed — see output above" exit 1 fi log "OK: MEG present and acceptance checklist passed" #!/usr/bin/env bash # Node exporter + textfile collector directory. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="12-monitoring" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [12-monitoring] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi TEXTFILE_DIR="/var/lib/node_exporter/textfile" if [[ "$CHECK_MODE" -eq 1 ]]; then systemctl is-enabled --quiet prometheus-node-exporter 2>/dev/null || systemctl is-enabled --quiet node_exporter 2>/dev/null || { log "FAIL(check): node exporter not enabled"; exit 1; } [[ -d "$TEXTFILE_DIR" ]] || { log "FAIL(check): textfile dir missing"; exit 1; } log "OK(check): node exporter enabled, textfile dir present" exit 0 fi export DEBIAN_FRONTEND=noninteractive dpkg -s prometheus-node-exporter >/dev/null 2>&1 || apt-get install -y prometheus-node-exporter install -d -m 755 "$TEXTFILE_DIR" # Ensure the textfile collector flag is set (package default may omit it). OVERRIDE="/etc/systemd/system/prometheus-node-exporter.service.d/blueforce.conf" mkdir -p "$(dirname "$OVERRIDE")" printf '[Service]\nExecStart=\nExecStart=/usr/bin/prometheus-node-exporter --collector.textfile.directory=%s\n' "$TEXTFILE_DIR" > /tmp/override.bf mv /tmp/override.bf "$OVERRIDE" systemctl daemon-reload systemctl enable --now prometheus-node-exporter 2>/dev/null || systemctl enable --now node_exporter log "OK: node exporter enabled with textfile dir $TEXTFILE_DIR" #!/usr/bin/env bash # Apply UFW deny-in baseline and persist Docker published-port policy. set -euo pipefail MODULE="13-firewall" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 SSH_PORT="${SSH_PORT:-22}" RDP_PORT="${RDP_PORT:-3389}" DOCKER_ALLOWLIST="/etc/blueforce/docker-published-port-allowlist.conf" DOCKER_POLICY_BIN="/usr/local/sbin/blueforce-docker-firewall" DOCKER_POLICY_UNIT="blueforce-docker-firewall.service" DOCKER_POLICY_DROPIN="/etc/systemd/system/docker.service.d/blueforce-docker-firewall.conf" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [13-firewall] $1" | tee -a "$LOG_FILE"; } if [[ "$CHECK_MODE" -eq 1 ]]; then ufw status 2>/dev/null | grep -q 'Status: active' || { log "FAIL(check): ufw not active"; exit 1; } ufw status 2>/dev/null | grep -Eq "${SSH_PORT}/tcp.*on wg0|${SSH_PORT}/tcp.*wg0" || { log "FAIL(check): SSH is not restricted to wg0"; exit 1; } ufw status 2>/dev/null | grep -Eq "${RDP_PORT}/tcp.*on wg0|${RDP_PORT}/tcp.*wg0" || { log "FAIL(check): RDP is not restricted to wg0"; exit 1; } [[ -f "$DOCKER_ALLOWLIST" && -x "$DOCKER_POLICY_BIN" && -f "$DOCKER_POLICY_DROPIN" ]] || { log "FAIL(check): persistent Docker firewall assets missing"; exit 1; } systemctl is-enabled --quiet "$DOCKER_POLICY_UNIT" || { log "FAIL(check): persistent Docker firewall unit is not enabled"; exit 1; } "$DOCKER_POLICY_BIN" --check || { log "FAIL(check): Docker published-port policy is not enforced"; exit 1; } log "OK(check): SSH/RDP restricted to wg0 and Docker policy persists through systemd" exit 0 fi export DEBIAN_FRONTEND=noninteractive command -v ufw >/dev/null 2>&1 || apt-get install -y ufw command -v iptables >/dev/null 2>&1 || apt-get install -y iptables ufw default deny incoming ufw default allow outgoing ufw delete allow "$SSH_PORT/tcp" 2>/dev/null || true ufw delete allow "$RDP_PORT/tcp" 2>/dev/null || true ufw allow in on wg0 to any port "$SSH_PORT" proto tcp comment 'SSH over WireGuard only' ufw allow in on wg0 to any port "$RDP_PORT" proto tcp comment 'RDP over WireGuard only' install -d -m 755 /etc/blueforce /etc/iptables if [[ ! -f "$DOCKER_ALLOWLIST" ]]; then cat > "$DOCKER_ALLOWLIST" <<'EOF' # Explicit Docker published-port exceptions. One reviewed entry per line: # tcp 8443 # udp 51820 # This allowlist intentionally permits no externally published Docker ports by default. EOF fi chown root:root "$DOCKER_ALLOWLIST" chmod 640 "$DOCKER_ALLOWLIST" install -o root -g root -m 700 "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" "$DOCKER_POLICY_BIN" install -o root -g root -m 644 "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" "/etc/systemd/system/$DOCKER_POLICY_UNIT" install -d -m 755 "$(dirname "$DOCKER_POLICY_DROPIN")" install -o root -g root -m 644 "$REPO_ROOT/config/systemd/docker.service.d/blueforce-docker-firewall.conf" "$DOCKER_POLICY_DROPIN" systemctl daemon-reload systemctl enable "$DOCKER_POLICY_UNIT" # Docker creates DOCKER-USER during startup; applying now proves the parsed policy. systemctl start docker "$DOCKER_POLICY_BIN" ufw --force enable log "OK: UFW allows SSH/RDP only on wg0; persisted Docker policy denies all unapproved published ports" #!/usr/bin/env bash # APT holds + disable automatic update timers. set -euo pipefail MODULE="14-update-policy" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 APT_HOLDS="${APT_HOLDS:-}" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [14-update-policy] $1" | tee -a "$LOG_FILE"; } if [[ "$CHECK_MODE" -eq 1 ]]; then for t in apt-daily.timer apt-daily-upgrade.timer; do systemctl is-enabled "$t" 2>/dev/null | grep -q masked || { log "FAIL(check): $t not masked"; exit 1; }; done log "OK(check): update timers masked"; exit 0 fi for t in apt-daily.service apt-daily.timer apt-daily-upgrade.service apt-daily-upgrade.timer; do systemctl mask "$t" >/dev/null 2>&1 || true; systemctl stop "$t" >/dev/null 2>&1 || true; done printf 'APT::Periodic::Update-Package-Lists "0";\nAPT::Periodic::Unattended-Upgrade "0";\nAPT::Periodic::Enable "0";\n' > /tmp/20auto-upgrades.bf mv /tmp/20auto-upgrades.bf /etc/apt/apt.conf.d/20auto-upgrades if [[ -n "$APT_HOLDS" ]]; then read -r -a apt_holds <<< "$APT_HOLDS" ((${#apt_holds[@]})) && apt-mark hold -- "${apt_holds[@]}" log "holds applied: ${apt_holds[*]}" fi log "OK: automatic updates locked off (timers masked, periodic=0)" #!/usr/bin/env bash # journald caps + app logrotate policy. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="15-logrotate" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [15-logrotate] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi JOURNAL_DROPIN="/etc/systemd/journald.conf.d/99-blueforce.conf" APP_ROTATE="/etc/logrotate.d/blueforce" if [[ "$CHECK_MODE" -eq 1 ]]; then [[ -f "$JOURNAL_DROPIN" && -f "$APP_ROTATE" ]] || { log "FAIL(check): logrotate configs missing"; exit 1; } log "OK(check): journald caps + app logrotate present" exit 0 fi mkdir -p /etc/systemd/journald.conf.d cat > /tmp/journald.bf <<'EOF' # Managed by blueforce-installer (15-logrotate). [Journal] SystemMaxUse=500M RuntimeMaxUse=100M MaxRetentionSec=30day ForwardToSyslog=no EOF mv /tmp/journald.bf "$JOURNAL_DROPIN" systemctl restart systemd-journald 2>/dev/null || true cat > /tmp/blueforce-rotate.bf <<'EOF' # Managed by blueforce-installer (15-logrotate). /var/log/blueforce*.log /var/log/bf-*.log { size 10M rotate 5 compress delaycompress missingok notifempty copytruncate } EOF mv /tmp/blueforce-rotate.bf "$APP_ROTATE" chmod 644 "$APP_ROTATE" logrotate --debug "$APP_ROTATE" 2>&1 | head -3 || true log "OK: journald capped (500M/30d), app logs rotate at 10M x5" #!/usr/bin/env bash # GNOME minimal and maintained GUI maintenance tools; terminal boot remains default. set -euo pipefail MODULE="16-gui" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [16-gui] $1" | tee -a "$LOG_FILE"; } if [[ "$CHECK_MODE" -eq 1 ]]; then dpkg -s ubuntu-desktop-minimal >/dev/null 2>&1 || { log "FAIL(check): ubuntu-desktop-minimal missing"; exit 1; } [[ -x /usr/local/bin/bf-gui-on && -x /usr/local/bin/bf-gui-off ]] || { log "FAIL(check): maintained bf-gui switches missing"; exit 1; } [[ -f /etc/systemd/system/bf-gui-on.service && -f /etc/systemd/system/bf-gui-off.service ]] || { log "FAIL(check): manual GUI service units missing"; exit 1; } [[ "$(systemctl get-default)" == multi-user.target ]] || { log "FAIL(check): terminal boot default was changed"; exit 1; } log "OK(check): GNOME minimal, maintained switches, and terminal boot default present"; exit 0 fi export DEBIAN_FRONTEND=noninteractive dpkg -s ubuntu-desktop-minimal >/dev/null 2>&1 || apt-get install -y ubuntu-desktop-minimal for tool in bf-gui-on bf-gui-off; do install -m 755 "$SCRIPT_ROOT/maintenance/$tool" "/usr/local/bin/$tool"; done for unit in bf-gui-on.service bf-gui-off.service; do install -m 644 "$SCRIPT_ROOT/../config/systemd/$unit" "/etc/systemd/system/$unit"; done systemctl daemon-reload systemctl set-default multi-user.target systemctl disable gdm3 2>/dev/null || systemctl disable gdm 2>/dev/null || true log "OK: maintained GUI tools and manual units installed; boot default is multi-user" #!/usr/bin/env bash # Install bf-status and run it for the first time. # Idempotent: safe to re-run. Supports --check for read-only verification. # Exit codes: 0 = OK, 3 = SKIP (optional step, nothing to do). set -euo pipefail MODULE="17-healthcheck" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" log() { local msg="$1" mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [17-healthcheck] $msg" | tee -a "$LOG_FILE" } CHECK_MODE=0 if [[ "${1:-}" == "--check" ]]; then CHECK_MODE=1; fi STATUS_BIN="/usr/local/bin/bf-status" write_bf_status() { cat > /tmp/bf-status.bf <<'EOF' #!/usr/bin/env bash # bf-status: quick device health summary. Exit 0 = healthy, 1 = problem. set -uo pipefail fails=0 say() { local st="$1"; shift; echo "[$st] $*"; [[ "$st" == "PASS" ]] || fails=$((fails+1)); } ID_FILE="/var/lib/blueforce/dealer-id" if [[ -f "$ID_FILE" && "$(cat $ID_FILE)" =~ ^[0-9]{8}$ ]]; then DID=$(cat "$ID_FILE") [[ "$(hostname)" == "bf-$DID" ]] && say PASS "identity BF-$DID / $(hostname)" || say FAIL "hostname $(hostname) != bf-$DID" else say FAIL "dealer-id file missing or invalid" fi systemctl is-enabled --quiet wg-quick@wg0 2>/dev/null && say PASS "wireguard enabled" || say FAIL "wireguard not enabled" systemctl is-active --quiet docker 2>/dev/null && say PASS "docker running" || say FAIL "docker not running" ufw status 2>/dev/null | grep -q "Status: active" && say PASS "firewall active" || say FAIL "firewall inactive" systemctl is-enabled apt-daily.timer 2>/dev/null | grep -q masked && say PASS "auto-update off" || say FAIL "auto-update timer live" systemctl is-enabled --quiet prometheus-node-exporter 2>/dev/null || systemctl is-enabled --quiet node_exporter 2>/dev/null && say PASS "monitoring enabled" || say FAIL "monitoring not enabled" echo "bf-status: $fails failing check(s)" exit "$fails" EOF mv /tmp/bf-status.bf "$STATUS_BIN" chmod 755 "$STATUS_BIN" } if [[ "$CHECK_MODE" -eq 1 ]]; then [[ -x "$STATUS_BIN" ]] || { log "FAIL(check): bf-status missing"; exit 1; } log "OK(check): bf-status installed" exit 0 fi mkdir -p "$STATE_DIR" if [[ -n "$DEALER_ID" ]]; then echo "$DEALER_ID" > "$STATE_DIR/dealer-id"; fi write_bf_status "$STATUS_BIN" | tee "$STATE_DIR/bf-status-first-run.log" >/dev/null || log "WARN: first bf-status run reports failures (see $STATE_DIR/bf-status-first-run.log)" log "OK: bf-status installed, first run saved" #!/usr/bin/env bash # Gate all installer modules; READY is written only when every required channel passes. set -euo pipefail MODULE="18-final-check" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" DEALER_ID="${DEALER_ID:-}" BF_HOSTNAME="${BF_HOSTNAME:-}" BF_DEVICE="${BF_DEVICE:-}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" CHECK_MODE=0 [[ "${1:-}" == "--check" ]] && CHECK_MODE=1 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GATES=(01-precheck 02-system 03-user 04-hostname 05-network 06-wireguard 07-ssh 08-rdp 09-rustdesk 10-docker 11-meg 12-monitoring 13-firewall 14-update-policy 15-logrotate 16-gui 17-healthcheck) READY_FILE="$STATE_DIR/READY" log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [18-final-check] $1" | tee -a "$LOG_FILE"; } self_gate() { [[ -n "$DEALER_ID" && "$DEALER_ID" =~ ^[0-9]{8}$ ]] && [[ "$(hostname)" == "bf-${DEALER_ID}" ]]; } run_gates() { local fails=0 skipped=0 out rc g for g in "${GATES[@]}"; do set +e; out=$(DEALER_ID="$DEALER_ID" BF_HOSTNAME="$BF_HOSTNAME" BF_DEVICE="$BF_DEVICE" STATE_DIR="$STATE_DIR" REQUIRE_WIREGUARD="${REQUIRE_WIREGUARD:-true}" BF_WG_HANDSHAKE_MAX_AGE="${BF_WG_HANDSHAKE_MAX_AGE:-180}" LOG_FILE=/dev/null bash "$SCRIPT_DIR/$g.sh" --check 2>&1); rc=$?; set -e case "$rc" in 0) echo "GATE $g: PASS";; 3) echo "GATE $g: SKIP (not READY)"; skipped=$((skipped+1)); fails=$((fails+1));; *) echo "GATE $g: FAIL"; echo "$out" | tail -3; fails=$((fails+1));; esac done if self_gate; then echo 'GATE 18-final-check: PASS'; else echo 'GATE 18-final-check: FAIL'; fails=$((fails+1)); fi echo "---"; echo "gates: 18 total, $fails failed, $skipped skipped"; return "$fails" } if [[ "$CHECK_MODE" -eq 1 ]]; then run_gates; exit $?; fi if run_gates; then install -d -m 700 "$STATE_DIR" printf 'READY dealer=BF-%s host=bf-%s date=%s\n' "$DEALER_ID" "$DEALER_ID" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$READY_FILE" log "OK: ALL REQUIRED GATES PASSED — device READY (BF-${DEALER_ID})" else log "FAIL: final-check gates failed — device NOT READY"; exit 1 fi #!/usr/bin/env bash # Blueforce one-click installer — single entry point for field setup. set -euo pipefail INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODULES_DIR="$INSTALLER_DIR/installer/modules" LOG_FILE="${LOG_FILE:-/var/log/blueforce-install.log}" STATE_DIR="${STATE_DIR:-/var/lib/blueforce}" STATE_FILE="$STATE_DIR/install-state" MODULES=(01-precheck 02-system 03-user 04-hostname 05-network 06-wireguard 07-ssh 08-rdp 09-rustdesk 10-docker 11-meg 12-monitoring 13-firewall 14-update-policy 15-logrotate 16-gui 17-healthcheck 18-final-check) DEALER_ID="" FROM="" ONLY="" CHECK_MODE=0 RESUME=0 ASSUME_YES=0 usage() { cat <<'EOF' Usage: sudo ./blueforce-install.sh --dealer-id <8 digits> [--from NN] [--only NAME] [--check] [--resume] [--yes] --check is read-only: it creates no logs, state, or directories. EOF } while [[ $# -gt 0 ]]; do case "$1" in --dealer-id) DEALER_ID="${2:-}"; shift 2;; --from) FROM="${2:-}"; shift 2;; --only) ONLY="${2:-}"; shift 2;; --check) CHECK_MODE=1; shift;; --resume) RESUME=1; shift;; --yes|-y) ASSUME_YES=1; shift;; --help|-h) usage; exit 0;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 2;; esac; done [[ -n "$DEALER_ID" ]] || { echo "Missing --dealer-id (8 digits)" >&2; exit 2; } [[ "$DEALER_ID" =~ ^[0-9]{8}$ ]] || { echo "Invalid dealer id '$DEALER_ID': must match ^[0-9]{8}$" >&2; exit 2; } log() { if [[ "$CHECK_MODE" -eq 1 ]]; then printf '[installer check] %s\n' "$1" >&2; else mkdir -p "$(dirname "$LOG_FILE")" "$STATE_DIR"; echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [installer] $1" | tee -a "$LOG_FILE"; fi; } record_state() { mkdir -p "$STATE_DIR"; local tmp="$STATE_FILE.tmp"; grep -v "^$1=" "$STATE_FILE" 2>/dev/null > "$tmp" || true; echo "$1=$2" >> "$tmp"; mv "$tmp" "$STATE_FILE"; } state_of() { grep "^$1=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || true; } export DEALER_ID BF_HOSTNAME="bf-${DEALER_ID}" BF_DEVICE="BF-${DEALER_ID}" STATE_DIR if [[ "$CHECK_MODE" -eq 0 && "$ASSUME_YES" -eq 0 ]]; then read -r -p "Proceed with installation as $BF_DEVICE? [y/N] " answer; [[ "$answer" =~ ^[Yy]$ ]] || { echo Aborted.; exit 2; }; fi RUN=(); for m in "${MODULES[@]}"; do if [[ -n "$ONLY" ]]; then [[ "$m" == "$ONLY" || "$m" == "$ONLY"* ]] && RUN+=("$m"); continue; fi [[ -n "$FROM" && "${m%%-*}" < "$FROM" ]] && continue if [[ "$CHECK_MODE" -eq 0 && "$RESUME" -eq 1 && "$(state_of "$m")" == OK ]]; then log "SKIP $m (already OK)"; continue; fi RUN+=("$m") done [[ ${#RUN[@]} -gt 0 ]] || { echo "No modules selected." >&2; exit 2; } log "=== install start dealer=$BF_DEVICE mode=$([[ "$CHECK_MODE" -eq 1 ]] && echo check || echo apply) modules=${RUN[*]} ===" failures=0 for m in "${RUN[@]}"; do script="$MODULES_DIR/$m.sh"; if [[ ! -x "$script" ]]; then log "FAIL $m (script missing)"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" FAIL; failures=$((failures+1)); break; fi args=(); [[ "$CHECK_MODE" -eq 1 ]] && args+=(--check) set +e; if [[ "$CHECK_MODE" -eq 1 ]]; then LOG_FILE=/dev/null bash "$script" "${args[@]}"; else LOG_FILE="$LOG_FILE" bash "$script"; fi; rc=$?; set -e case "$rc" in 0) log "OK $m"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" OK;; 3) log "SKIP $m"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" SKIP;; *) log "FAIL $m (exit $rc)"; [[ "$CHECK_MODE" -eq 0 ]] && record_state "$m" FAIL; failures=$((failures+1)); [[ "$CHECK_MODE" -eq 0 ]] && exit 1;; esac done [[ "$CHECK_MODE" -eq 1 && "$failures" -gt 0 ]] && exit 1 log "=== install done dealer=$BF_DEVICE failures=$failures ===" # Blueforce installer (`scripts/install/`) Single entry point: `sudo ./blueforce-install.sh --dealer-id <8 digits>`. - Orchestrator: `blueforce-install.sh` (dealer-id gate `^[0-9]{8}$`, hostname `bf-` + device `BF-`, ordered module runs, `--resume`, results to `/var/log/blueforce-install.log` + `/var/lib/blueforce/install-state`). - Modules: `installer/modules/01-*.sh` … `18-*.sh`. Every module is idempotent and supports `--check` (read-only audit). Exit `0` = OK, `3` = SKIP. - Required remote-channel preconditions: set per-device `WG_ADDRESS`, `WG_ENDPOINT`, and `WG_PEER_PUBKEY`; WireGuard readiness requires a peer handshake no older than `BF_WG_HANDSHAKE_MAX_AGE` seconds (default `180`) unless `REQUIRE_WIREGUARD=false`. Set `RUSTDESK_SERVER` to a real self-hosted endpoint and provide the pinned local `RUSTDESK_DEB`; placeholders are rejected. - Docker published ports default to deny. Add only reviewed `tcp PORT` or `udp PORT` entries to `/etc/blueforce/docker-published-port-allowlist.conf`; the enabled `blueforce-docker-firewall.service` reapplies that policy after Docker starts. - Exit codes: `0` READY · `1` module failure · `2` argument/format error. Module map: 01-precheck · 02-system · 03-user · 04-hostname · 05-network · 06-wireguard · 07-ssh · 08-rdp · 09-rustdesk · 10-docker · 11-meg · 12-monitoring · 13-firewall · 14-update-policy · 15-logrotate · 16-gui · 17-healthcheck · 18-final-check. NOTE on AnyDesk: there is deliberately no AnyDesk module. Remote access is covered by SSH-over-WireGuard, xRDP and RustDesk. AnyDesk stays a manual, per-device exception outside this installer. #!/usr/bin/env bash # # bf-gui-off - Switch the field terminal back to console (multi-user) mode. # # Stops graphical services (xRDP + display manager) and isolates # multi-user.target. Idempotent: already-console systems are left untouched. # # Install: install -m 0755 bf-gui-off /usr/local/bin/bf-gui-off # Usage: sudo bf-gui-off # set -euo pipefail usage() { cat <<'EOF' Usage: bf-gui-off [--help] Stop the graphical layer and return the device to console mode. Options: --help Show this help and exit Behavior: - Stops xrdp, xrdp-sesman and the display manager (gdm3/gdm/...) - Disables them so they do not restart on boot - Runs 'systemctl isolate multi-user.target' EOF } while [[ $# -gt 0 ]]; do case "$1" in --help|-h) usage; exit 0 ;; *) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [[ "$(id -u)" -ne 0 ]]; then echo "ERROR: bf-gui-off must run as root (use sudo)." >&2 exit 1 fi log() { echo "[bf-gui-off] $*"; } UNITS=(xrdp xrdp-sesman gdm3 gdm sddm lightdm) for unit in "${UNITS[@]}"; do if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${unit}"; then if systemctl is-active --quiet "${unit}"; then log "Stopping ${unit} ..." systemctl stop "${unit}" || log "WARNING: failed to stop ${unit}." else log "${unit} already stopped." fi systemctl disable "${unit}" >/dev/null 2>&1 || true fi done log "Isolating multi-user.target ..." systemctl isolate multi-user.target log "GUI is OFF (console mode)." #!/usr/bin/env bash # # bf-gui-on - Switch the field terminal to graphical mode and prepare RDP. # # Enables and starts the display manager plus xRDP services, isolates the # graphical target, then waits until RDP is ready to accept sessions. # Idempotent: already-graphical systems are left untouched. # # Install: install -m 0755 bf-gui-on /usr/local/bin/bf-gui-on # Usage: sudo bf-gui-on [--timeout SECS] # set -euo pipefail TIMEOUT=60 DISPLAY_MANAGER="" usage() { cat <<'EOF' Usage: bf-gui-on [--timeout SECS] [--help] Switch the device to graphical mode and wait until RDP is ready. Options: --timeout SECS Seconds to wait for RDP port 3389 (default: 60) --help Show this help and exit Behavior: - Enables and starts the display manager (gdm3/gdm) + xrdp + xrdp-sesman - Runs 'systemctl isolate graphical.target' - Waits until TCP/3389 accepts connections (RDP ready standby) EOF } while [[ $# -gt 0 ]]; do case "$1" in --timeout) TIMEOUT="${2:?missing value for --timeout}"; shift 2 ;; --help|-h) usage; exit 0 ;; *) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [[ "$(id -u)" -ne 0 ]]; then echo "ERROR: bf-gui-on must run as root (use sudo)." >&2 exit 1 fi log() { echo "[bf-gui-on] $*"; } # Detect installed display manager unit (LAB validates exact names on 26.04.1). detect_dm() { local candidate for candidate in gdm3.service gdm.service sddm.service lightdm.service; do if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${candidate}"; then echo "${candidate%.service}" return 0 fi done return 1 } if DISPLAY_MANAGER="$(detect_dm)"; then log "Display manager detected: ${DISPLAY_MANAGER}" else log "WARNING: no display manager unit found; proceeding with xrdp only." fi UNITS=() [[ -n "${DISPLAY_MANAGER}" ]] && UNITS+=("${DISPLAY_MANAGER}") UNITS+=(xrdp xrdp-sesman) # Enable units that exist so GUI survives reboot while switched on. for unit in "${UNITS[@]}"; do if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${unit}"; then systemctl enable "${unit}" >/dev/null 2>&1 || true else log "WARNING: unit ${unit} not installed; skipping." fi done CURRENT="$(systemctl get-default 2>/dev/null || echo unknown)" log "Current default target: ${CURRENT}" log "Isolating graphical.target ..." systemctl isolate graphical.target for unit in "${UNITS[@]}"; do if systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${unit}"; then if ! systemctl is-active --quiet "${unit}"; then log "Starting ${unit} ..." systemctl start "${unit}" || log "WARNING: failed to start ${unit}." else log "${unit} already active." fi fi done # Wait for RDP standby on TCP/3389. log "Waiting for RDP (TCP/3389) up to ${TIMEOUT}s ..." elapsed=0 while [[ "${elapsed}" -lt "${TIMEOUT}" ]]; do if (exec 3<>/dev/tcp/127.0.0.1/3389) >/dev/null 2>&1; then exec 3>&- 2>/dev/null || true log "RDP is ready (graphical ON)." exit 0 fi sleep 2 elapsed=$((elapsed + 2)) done echo "ERROR: RDP port 3389 did not open within ${TIMEOUT}s." >&2 echo "HINT: check 'systemctl status xrdp xrdp-sesman' and journal." >&2 exit 1 #!/usr/bin/env bash # Parse fleet configuration and validate executable shell scripts without running them. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" FAIL=0 fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; } pass() { printf 'OK: %s\n' "$*"; } # Every Bash script under scripts/ must pass syntax parsing. Non-Bash assets are ignored. while IFS= read -r -d '' script; do if head -n 1 "$script" | grep -qE '^#!/usr/bin/env bash|^#!/bin/bash'; then bash -n "$script" && pass "bash parses: ${script#$REPO_ROOT/}" || fail "bash parse error: ${script#$REPO_ROOT/}" fi done < <(find "$REPO_ROOT/scripts" -type f -print0) # All Ansible and deployable configuration YAML must parse through PyYAML safe_load. while IFS= read -r -d '' yaml_file; do if python3 - "$yaml_file" <<'PY' import sys import yaml with open(sys.argv[1], encoding='utf-8') as handle: yaml.safe_load(handle) PY then pass "yaml parses: ${yaml_file#$REPO_ROOT/}" else fail "yaml parse error: ${yaml_file#$REPO_ROOT/}" fi done < <(find "$REPO_ROOT/ansible" "$REPO_ROOT/config" "$REPO_ROOT/monitoring" -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) # JSON configuration must parse as JSON, not merely as JavaScript. while IFS= read -r -d '' json_file; do if python3 - "$json_file" <<'PY' import json import sys with open(sys.argv[1], encoding='utf-8') as handle: json.load(handle) PY then pass "json parses: ${json_file#$REPO_ROOT/}" else fail "json parse error: ${json_file#$REPO_ROOT/}" fi done < <(find "$REPO_ROOT/config" "$REPO_ROOT/monitoring" -type f -name '*.json' -print0) # Required values preserve the naming and wave policy in docs/09 and docs/10. INV="$REPO_ROOT/ansible/inventory/hosts.example.yml" GV="$REPO_ROOT/ansible/inventory/group_vars/all.yml" [[ -f "$INV" ]] || fail "missing inventory example: $INV" [[ -f "$GV" ]] || fail "missing shared group variables: $GV" for group in lab pilot_1 pilot_2 wave_1 wave_2 production; do grep -q "^[[:space:]]*$group:" "$INV" || fail "inventory missing wave group: $group" done for key in wg_subnet wg_interface monitoring_endpoint meg_image_tag reboot_grace_seconds support_bundle_dir; do grep -q "^$key:" "$GV" || fail "group_vars/all.yml missing key: $key" done for pb in bf-collect-logs.yml bf-gui-on.yml bf-gui-off.yml bf-run-script.yml; do grep -q '^ hosts: "{{ operation_wave' "$REPO_ROOT/ansible/playbooks/$pb" || fail "$pb must dynamically target operation_wave" grep -q 'production_override: false' "$REPO_ROOT/ansible/playbooks/$pb" || fail "$pb must default production_override to false" done FIREWALL_POLICY="$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" bash -n "$FIREWALL_POLICY" && pass 'bash parses: config/firewall/blueforce-docker-user-restore.sh' || fail 'bash parse error: config/firewall/blueforce-docker-user-restore.sh' grep -q '^After=docker.service$' "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" || fail 'Docker firewall service must wait for Docker' grep -q '^Wants=blueforce-docker-firewall.service$' "$REPO_ROOT/config/systemd/docker.service.d/blueforce-docker-firewall.conf" || fail 'Docker drop-in must start firewall policy' if grep -RniE --include='*.yml' --include='*.yaml' --include='*.json' '^[[:space:]]*[^#].*(image:[^#]*:latest([[:space:]#]|$)|watchtower)' "$REPO_ROOT/ansible" "$REPO_ROOT/config" "$REPO_ROOT/monitoring"; then fail "forbidden moving tag or Watchtower configuration found" fi if [[ "$FAIL" -ne 0 ]]; then printf 'check-configs: FAILED\n' >&2 exit 1 fi printf 'check-configs: all passed\n' #!/usr/bin/env bash # Validate repository specifications and Ansible safety contracts. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PB="$REPO_ROOT/ansible/playbooks" ROLES="$REPO_ROOT/ansible/roles" FAIL=0 fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; } pass() { printf 'OK: %s\n' "$*"; } # The canonical documentation set is exactly docs/00 through docs/25. mapfile -t DOCS < <(printf '%s\n' "$REPO_ROOT"/docs/[0-9][0-9]-*.md | sort) if [[ ${#DOCS[@]} -ne 26 ]]; then fail "expected 26 numbered docs, found ${#DOCS[@]}" else for number in $(seq -w 0 25); do matches=("$REPO_ROOT"/docs/"$number"-*.md) [[ -f ${matches[0]} ]] || fail "missing numbered doc: $number" done pass "26 numbered docs present" fi # Each numbered doc must retain ordered sections 1 through 13 and at least one diagram. mermaid_total=0 for doc in "${DOCS[@]}"; do heading_list="$(grep -E '^## [0-9]+\.' "$doc" | sed -E 's/^## ([0-9]+)\..*/\1/' | paste -sd, -)" if [[ "$heading_list" != '1,2,3,4,5,6,7,8,9,10,11,12,13' ]]; then fail "non-sequential numbered headings in ${doc#$REPO_ROOT/}: ${heading_list:-none}" fi count="$(grep -c '^```mermaid$' "$doc" || true)" if [[ "$count" -lt 1 ]]; then fail "missing Mermaid block in ${doc#$REPO_ROOT/}" fi mermaid_total=$((mermaid_total + count)) done if [[ "$mermaid_total" -ne 28 ]]; then fail "expected 28 Mermaid blocks across docs, found $mermaid_total" else pass "28 Mermaid blocks present" fi EXPECTED_PLAYBOOKS=(bf-ping.yml bf-uptime.yml bf-reboot.yml bf-docker-status.yml bf-service-restart.yml bf-wireguard-restart.yml bf-rustdesk-restart.yml bf-rdp-restart.yml bf-collect-logs.yml bf-disk-check.yml bf-package-check.yml bf-security-updates-check.yml bf-deploy-update.yml bf-run-script.yml bf-gui-on.yml bf-gui-off.yml) for pb in "${EXPECTED_PLAYBOOKS[@]}"; do [[ -f "$PB/$pb" ]] && pass "playbook present: $pb" || fail "missing playbook: $pb" done for role in common wireguard docker monitoring; do for file in tasks/main.yml defaults/main.yml README.md; do [[ -f "$ROLES/$role/$file" ]] && pass "role file present: $role/$file" || fail "missing role file: $role/$file" done done # Safety contracts from docs/09 and docs/10. grep -q 'serial' "$PB/bf-reboot.yml" || fail 'bf-reboot.yml must batch hosts with serial' grep -q 'any_errors_fatal: true' "$PB/bf-reboot.yml" || fail 'bf-reboot.yml must halt on errors' grep -q 'release_wave' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a release wave' grep -q 'wave_gate_confirmed' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a wave gate' grep -q 'halt_on_fail' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must enforce halt_on_fail' grep -q 'max_fail_percentage: 0' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must stop the wave on failure' grep -q 'serial: 5' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must use the fixed batch size' grep -q 'production_override' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a separate production override' grep -q 'ansible_play_hosts_all' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must verify selected hosts against the release wave' grep -q 'Run mandatory post-batch health gate' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must run a post-batch health gate' for pb in bf-collect-logs.yml bf-gui-on.yml bf-run-script.yml bf-gui-off.yml; do grep -q 'operation_wave' "$PB/$pb" || fail "$pb must require operation_wave" grep -q 'operation_confirmed' "$PB/$pb" || fail "$pb must require operation_confirmed" grep -q 'ansible_play_hosts_all' "$PB/$pb" || fail "$pb must verify its selected hosts" grep -q 'production_override' "$PB/$pb" || fail "$pb must require a separate production override" grep -q "operation_wave != 'production' or production_override | bool" "$PB/$pb" || fail "$pb must refuse production without production_override=true" done if grep -q '^ hosts: blueforce$' "$PB/bf-gui-off.yml"; then fail 'bf-gui-off.yml must not target the blueforce group by default' fi grep -q '^operation_wave:' "$REPO_ROOT/ansible/inventory/group_vars/all.yml" || fail 'shared operation_wave variable missing' grep -q '^operation_confirmed:' "$REPO_ROOT/ansible/inventory/group_vars/all.yml" || fail 'shared operation_confirmed variable missing' for asset in config/firewall/blueforce-docker-user-restore.sh config/systemd/blueforce-docker-firewall.service config/systemd/docker.service.d/blueforce-docker-firewall.conf; do [[ -f "$REPO_ROOT/$asset" ]] || fail "missing persistent Docker firewall asset: $asset" done grep -q 'systemctl enable "$DOCKER_POLICY_UNIT"' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module must enable persistent Docker firewall unit' grep -q 'docker.service.d/blueforce-docker-firewall.conf' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module must install Docker restart coupling' grep -q 'DOCKER_POLICY_BIN" --check' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module check must verify Docker policy rules' grep -q '13-firewall' "$REPO_ROOT/scripts/install/installer/modules/18-final-check.sh" || fail 'final check must include the firewall rule verification gate' grep -q 'bash "$SCRIPT_DIR/$g.sh" --check' "$REPO_ROOT/scripts/install/installer/modules/18-final-check.sh" || fail 'final check must execute every gate in check mode' grep -q 'ctorigdstport' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall must consume published-port allowlist rules' grep -q 'iptables -C DOCKER-USER' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall check must verify every installed allowlist rule' grep -q -- '-i br+ -j RETURN' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall must permit egress from user-defined bridge interfaces' grep -q '^BindsTo=docker.service$' "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" || fail 'Docker firewall service must bind to Docker lifecycle' grep -q '^PartOf=docker.service$' "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" || fail 'Docker firewall service must restart with Docker' grep -q '^Wants=blueforce-docker-firewall.service$' "$REPO_ROOT/config/systemd/docker.service.d/blueforce-docker-firewall.conf" || fail 'Docker must start the firewall service on every start' grep -q 'BF_WG_HANDSHAKE_MAX_AGE' "$REPO_ROOT/scripts/install/installer/modules/06-wireguard.sh" || fail 'WireGuard module must enforce handshake freshness' grep -q 'configured ||' "$REPO_ROOT/scripts/install/installer/modules/09-rustdesk.sh" || fail 'RustDesk module must prove client configuration' grep -q 'service_allowlist' "$PB/bf-service-restart.yml" || fail 'bf-service-restart.yml must enforce an allowlist' grep -q 'script_allowed_dirs' "$PB/bf-run-script.yml" || fail 'bf-run-script.yml must enforce allowed script paths' grep -Fq "collect_logs_dir is match('^/var/lib/blueforce/support-bundles" "$PB/bf-collect-logs.yml" || fail 'bf-collect-logs.yml must restrict bundles to the approved support directory' if [[ "$(grep -Fc '| quote' "$PB/bf-collect-logs.yml")" -lt 2 ]]; then fail 'bf-collect-logs.yml must shell-quote every generated bundle path' fi if grep -RniE --include='*.yml' '^[[:space:]]*[^#].*(image:[^#]*:latest([[:space:]#]|$)|watchtower)' "$PB" "$ROLES/docker"; then fail "forbidden moving tag or Watchtower configuration found in deploy paths" else pass 'no moving tags or auto-updaters in deploy paths' fi if grep -qniE 'state:[[:space:]]*(present|latest)|apt-get .*(upgrade|dist-upgrade)[[:space:]]+-y|apt:.*name:' "$PB/bf-security-updates-check.yml"; then fail 'bf-security-updates-check.yml must remain report-only' else pass 'security update check is report-only' fi if [[ "$FAIL" -ne 0 ]]; then printf 'check-specs: FAILED\n' >&2 exit 1 fi printf 'check-specs: all passed\n' # Semaphore Community limits (working rules for the Blueforce fleet) Source: `docs/09-FLEET-MANAGEMENT.md` section 9, "Semaphore Community sinirlari". This file restates the limits in English as an executable checklist for playbook authors and Semaphore operators. No Pro/Enterprise feature may be used anywhere in `ansible/` or in any Semaphore task template. ## Limits 1. No OIDC/SSO: Semaphore users are local accounts; access only behind WireGuard. 2. No 2FA: password policy + mandatory VPN compensates; admin accounts are inventoried. 3. No external Vault integration: secrets live in the Semaphore built-in encrypted Key Store; production secrets are never embedded in playbooks. 4. Concurrency limits (runners/forks): load-test against 700 nodes first, then tune runner count + Ansible `serial`/`forks`/batch settings. Destructive playbooks (`bf-reboot.yml`, `bf-deploy-update.yml`) already enforce `serial` + `max_fail_percentage: 0` + `any_errors_fatal: true`. 5. No workflow may depend on a Pro-only feature. Anything that does not run on Community is rejected in review (`tests/check-specs.sh` greps for it). ## Operator checklist - [ ] Semaphore reachable only via WireGuard. - [ ] Every destructive template runs `--check` dry run + `--limit ` first. - [ ] Waves follow docs/10: lab(2) -> pilot-1(5) -> pilot-2(20) -> wave-1(50) -> wave-2(100) -> production. - [ ] Secrets referenced from Key Store, never committed to Git. - [ ] `tests/check-specs.sh` and `tests/check-configs.sh` pass before merge. # > Kısa özet: bu doküman neyi kapsar, kimin içindir, hangi kararı verir. (2–3 cümle) - Dosya: `docs/NN-KONU.md` - İlgili kararlar: `24-DECISION-LOG.md#` - Durum: [ ] Taslak / [ ] İncelemede / [ ] Onaylı --- ## 1. Amaç Bu dokümanın neden var olduğu, hangi sorunu çözdüğü. ## 2. Kapsam Kapsam içi ve kapsam dışı maddeler: - Kapsam içi: ... - Kapsam dışı: ... ## 3. Kararlar Her teknik karar için aşağıdaki karar bloğunu kullan: ```text KARAR: GEREKÇE: ALTERNATİF: RİSK: MALİYET: <Ücretsiz / Ücretli (işaretli) + not> LİSANS: ``` Örnek: ```text KARAR: Filo yönetiminde Ansible + Semaphore UI Community kullanılır. GEREKÇE: Agent gerektirmez, SSH tabanlıdır, 700 cihaz ölçeğinde yeterlidir. ALTERNATİF: SaltStack — daha karmaşık kurulum nedeniyle elendi. RİSK: Playbook hatası filoya yayılabilir; önce LAB(2) grubunda çalıştırılır. MALİYET: Ücretsiz (self-hosted). LİSANS: Semaphore Community (kaynak doğrulanacak) — URL + sürüm + tarih eklenecek. ``` ## 4. Neden Bu Karar? Kararların arkasındaki teknik ve operasyonel gerekçeler. Sadelik kuralı burada denetlenir: Kubernetes, microservice, gereksiz agent/DB/cloud öneriliyorsa neden reddedildiği yazılır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | ... | ... | ... | Elendi / Yedek | ## 6. Avantajlar - ... ## 7. Dezavantajlar - ... ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | ... | Düşük/Orta/Yüksek | ... | ... | ## 9. Uygulama Planı Numaralı adımlar; komutlar İngilizce orijinal adıyla, açıklamalar Türkçe: 1. ... 2. ... ```bash # örnek komut bloğu hostnamectl set-hostname bf-12010193 ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | ... | ... | LAB(2) | ## 11. Rollback Başarısızlıkta önceki duruma dönüş adımları: 1. ... 2. ... ## 12. Kontrol Listesi - [ ] ... - [ ] ... ## 13. Açık Sorular - [ ] ... (sahibi + tarih ile) --- ## Ek: Mermaid Örneği ```mermaid flowchart LR Saha["bf-12010193
(saha PC'si)"] -->|WireGuard| Merkez["Merkez sunucu"] Merkez --> Ansible["Ansible/Semaphore"] Merkez --> Monitor["Monitoring"] ```
# 00 — Ana Plan (Master Plan) > Kısa özet: 700 cihazlık Blueforce Linux filosunun uçtan uca planı: hangi doküman neyi kapsar, fazlar nasıl ilerler, tüm işler hangi kararlara dayanır. Projeye yeni katılan herkes önce bu dosyayı okur. - Dosya: `docs/00-MASTER-PLAN.md` - İlgili kararlar: `24-DECISION-LOG.md#K-01…K-14` (tüm fazlar bu kararlara dayanır) - Durum: [x] Onaylı (Faz 2 kilidi) --- ## 1. Amaç 700 saha PC'sinin Ubuntu tabanlı standardize filoya dönüşümünü tek planda toplamak: kapsam, fazlar, doküman haritası ve başarı kriterleri. Parça işlerin (imaj, kurucu, ağ, filo, monitoring) birbirine nasıl bağlandığı bu dosyadan izlenir. ## 2. Kapsam - Kapsam içi: faz planı (LAB → PİLOT → WAVE → PROD), doküman haritası (00…24), karar bağımlılıkları (K-01…K-14), dalga boyutları ve kapı kriterleri. - Kapsam dışı: kurulum komutları (03/04/05), playbook ve metrik detayları (09/14), VDS kurulum adımları (07/08) — ilgili dokümanlara havale. ## 3. Kararlar ```text KARAR: Proje Faz 2'de kilitlenen 14 karara (K-01…K-14) göre yürütülür; karar değişikliği yalnızca 24-DECISION-LOG PR'ı ile yapılır. GEREKÇE: 700 cihazda tutarlılığın tek dayanağı yazılı karardır; her doküman kararlara linklenir, varsayıma dayalı seçim bırakılmaz. ALTERNATİF: Doküman başına bağımsız karar — çelişki ve tekrara yol açtığı için elendi. RİSK: Karar kilidi gelişmeyi yavaşlatabilir; azaltma: LAB/PİLOT bulguları PR ile hızlı işletilir. MALİYET: Ücretsiz (süreç kararı). LİSANS: Yok (yöntem kararı). ``` ```text KARAR: Dağıtım dalgaları: LAB(2) → PİLOT-1(5) → PİLOT-2(20) → WAVE-1(50) → WAVE-2(100) → PROD (kalan ~523). GEREKÇE: Onaysız update yasağı (K-11) gereği her değişiklik küçük dalgada kanıtlanmadan büyüğe geçmez; blast-radius üstten sınırlanır. ALTERNATİF: Tek seferde tüm filo — tek hatanın 700 cihaza yayılması nedeniyle elendi. RİSK: Dalga disiplini atlanırsa hata büyür; azaltma: Semaphore'da onay adımı + 10-UPDATE kapı kriterleri. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Öncelik sırası (kesintisiz çalışma > veri kaybı yok > uzaktan erişim > bakım kolaylığı) planı şekillendirdi. Tüm kararlarda üç filtre uygulandı: (1) %100 ücretsiz/self-hosted, (2) sadelik (K8s, gereksiz agent/DB/cloud yok), (3) onaysız update yasağı. 700 ölçeğinde her küçük varsayılan filo-çapı arızaya dönüşür; fazlar bu çarpanı küçültür. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Big-bang dağıtım | Hızlı | Tek hata = 700 cihaz arızası | Elendi | | K8s-orkestrasyonlu saha | Esnek ölçek | 700 mini PC'de gereksiz yük | Elendi | | Cloud-merkezli yönetim | Hızlı kurulum | Kota/lisans maliyeti, veri egemenliği | Elendi | ## 6. Avantajlar - Tek kimlik (`BF-`/`bf-`) tüm katmanlarda join anahtarı — envanterden monitoring'e aynı ad. - Her fazın giriş/çıkış kriteri yazılı; "hazır" demek ölçülebilir. - Hiçbir faz ücretli lisansa dayanmaz (FREE sürümler baz). ## 7. Dezavantajlar - Faz disiplini ilk kurulumu yavaşlatır (LAB+PİLOT ölçümleri beklenir). - Merkez servis sayısı ilk kurulumda tek VDS'e sığsa da yük testinden geçmeli (sahibi: 25-ROADMAP). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Faz atlanarak sahaya çıkılması | Orta | Yüksek | Kapı kriterleri + Semaphore onay adımı | | Merkez VDS kesintisi | Düşük | Yüksek | Hızlı yeniden kurulum + yedek VDS planı (17) | | Heterojen donanımda imaj kırılması | Orta | Orta | Envanter taraması + LAB profil testi (04) | ## 9. Uygulama Planı 1. Faz 1 (Zemin): karar kilidi (24) + mimari (01) + bu plan — tamamlandı. 2. Faz 2 (İmaj+Kurucu): 02-03-04-05 dokümanları + `blueforce-install.sh` + LAB(2) imaj testi. 3. Faz 3 (Merkez): WireGuard hub → hbbs/hbbr → MeshCentral → Semaphore → Prometheus/Grafana/Kuma (07/08/09/14). 4. Faz 4 (PİLOT): P1(5)+P2(20) cihazda 7/24 sayaçlar (kopma, log hacmi, metrik kardinalitesi). 5. Faz 5 (WAVE+PROD): W1(50) → W2(100) → PROD (~523), her dalgada 10-UPDATE onay zinciri. ```bash # kapı örneği: LAB'dan P1'e geçiş koşulu ./scripts/install/blueforce-install.sh --check # exit 0 olmadan dalga büyümez ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Her dokümanın karar linkleri | Tüm karar blokları 24'teki K-no'ya bağlı | Repo CI (markdown-link-check) | | LAB(2) uçtan uca akış | Erişim + boot + update zinciri çalışır | LAB(2) | | Dalga kapı kriterleri | Kriter sağlanmadan sonraki dalga açılmaz | P1/P2/W1/W2 | ## 11. Rollback 1. Plan değişikliği 24-DECISION-LOG PR'ı ile yapılır; bağımlı dokümanlar aynı PR'da güncellenir. 2. Sahaya sürülmüş dalga hatalıysa 10-UPDATE rollback zinciri işletilir. 3. Son çare: karar öncesi imaj sürümüne dönüş (17-RECOVERY LEVEL 9). ## 12. Kontrol Listesi - [ ] 14 kararın her biri ≥1 dokümana ve faza bağlı. - [ ] Dalga boyutları tüm dokümanlarda tutarlı (2/5/20/50/100/~523). - [ ] FREE sürüm kuralı tüm fazlarda korunuyor. - [ ] `SUMMARY.md` Faz 4'te 27 soru → karar izlenebilirliğiyle üretilecek. ## 13. Açık Sorular - [ ] Merkez servislerin tek VDS'e sığıp sığmadığı — LAB yük testi (sahibi: 25-ROADMAP). - [ ] Faz takviminin bayi rollout kapasitesine göre netleşmesi (sahibi: 18-ROLLOUT). --- ## Ek: Doküman Haritası ```mermaid flowchart LR P0["00 Master Plan
(bu dosya)"] --> ARCH["01 Mimari
24 Karar Log"] ARCH --> ZEMIN["02 Kimlik + Envanter
03 Ubuntu Baseline
04 Golden Image
05 Tek-Kurucu"] ZEMIN --> MERKEZ["06 SSH 07 Erişim 08 WireGuard
09 Filo 11 Hardening"] MERKEZ --> OPS["10 Update 12 Docker
14 Monitoring 15 Logging 16 Boot
17 Recovery 18 Rollout"] OPS --> GOV["19 Test 20 Runbook 21 CI
22 Docs 23 Lisans"] ``` | Doküman | Konu | Dayandığı karar | |---|---|---| | 01 | Sistem mimarisi | K-01…K-14 | | 02 | Cihaz adlandırma + envanter | K-12 | | 03 | Ubuntu baseline | K-01, K-02, K-03, K-11 | | 04 | Golden image + provisioning | K-09 | | 05 | Tek-tık kurucu | K-09, K-11, K-12 | | 06–09 | SSH, erişim, WireGuard, filo | K-13, K-04, K-05, K-06 | | 10–12 | Update, hardening, Docker | K-11, K-10 | | 13–16 | MEG, monitoring, logging, boot | K-02, K-07, K-14, K-03 | | 17–18 | Recovery, rollout | K-04, K-09 | | 19–23 | Test, runbook, CI, docs, lisans | K-08 + tümü | | 24 | Karar günlüğü | Kilit kaynak |
# 01 — Sistem Mimarisi > Kısa özet: 700 saha cihazlı Blueforce filosunun uçtan uca mimarisi: saha PC'si, WireGuard ağı, 4 erişim kanalı, filo otomasyonu, monitoring ve update zinciri tek bakışta. Kararların görsel karşılığıdır. > > - Dosya: `docs/01-ARCHITECTURE.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-01…K-14` (tüm mimari bu kararlara dayanır) > - Durum: [x] Onaylı (Faz 2 kilidi) --- ## 1. Amaç Başka bir DevOps mühendisinin filoyu tek dosyadan anlayıp kurmaya başlayabilmesi: hangi bileşen nerede koşar, trafik nasıl akar, cihaz nasıl boot eder, update nasıl yayılır. ## 2. Kapsam - Kapsam içi: saha cihazı katmanları, merkez (VDS) servisleri, ağ/erişim akışı, boot zinciri, update onay akışı. - Kapsam dışı: kurulum komutları (03/04/05), playbook detayları (09), metrik listesi detayı (14) — ilgili dokümanlara havale. ## 3. Kararlar ```text KARAR: Filo mimarisi: saha = Ubuntu Server 26.04.1+ GNOME + Docker(MEG) + WireGuard spoke; merkez = WireGuard hub + RustDesk hbbs/hbbr + MeshCentral + Ansible/Semaphore + Prometheus/Grafana + Uptime Kuma (hepsi self-hosted, ücretsiz katman). GEREKÇE: 24-DECISION-LOG K-01…K-08'in birleşimi; hareketli parça en aza indirildi (agent'sız filo, tek Go binary UI, tek konteyner uptime), iki WireGuard-bağımsız erişim kanalıyla tek-nokta körlüğü engellendi. ALTERNATİF: K8s-tabanlı yönetim / Cloud-merkezli monitoring — sadelik kuralını bozduğu için elendi (bkz. 24 §4). RİSK: Merkez VDS tek fiziksel nokta; azaltma: yedekleme + hızlı yeniden kurulum (17-RECOVERY), kritik kanalların ayrı VDS'e taşınabilirliği. MALİYET: Ücretsiz (lisans $0; VDS kira bedeli altyapı maliyetidir). LİSANS: Bileşen lisansları için 24-DECISION-LOG Ek A'ya bakılır. ``` ## 4. Neden Bu Karar? Öncelik sırası (kesintisiz çalışma > veri kaybı yok > uzaktan erişim > bakım kolaylığı) mimariyi şekillendirdi: boot zinciri her katmanda otomatik toparlanır (systemd + `unless-stopped` + `wg-quick` enable), erişim 4 kanalla yedeklenir (ikisi WireGuard-bağımsız), update'ler onay zincirinden geçmeden sahaya inmez. Hiçbir katman ücretli lisansa dayanmaz. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Tek kanal (yalnızca SSH) | En sade | Tünel çökerse körlük | Elendi | | Cloud monitoring (Netdata Cloud vb.) | Hızlı kurulum | 5 node free kotası, 700 cihaz ücretli | Elendi | | K8s-orkestrasyonlu saha | Ölçek esnekliği | 700 mini PC'de gereksiz yük | Elendi | ## 6. Avantajlar - Tek kimlik (`BF-`/`bf-`) tüm katmanlarda join anahtarı — envanterden monitoring'e aynı ad. - Her akış (erişim/boot/update) diyagramla sabit; runbook'lar bu diyagramlara prosedür yazar. ## 7. Dezavantajlar - Merkez servis sayısı (hub + 2 erişim + fleet + 2 monitoring + docs) ilk kurulumda tek VDS'e sığsa da yük testinden geçmeli. - Diyagramlar sözleşmedir: bileşen değişirse 3 Mermaid de aynı PR'da güncellenmeli. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Merkez VDS kesintisi | Düşük | Yüksek | Hızlı yeniden kurulum + yedek VDS planı (17) | | Boot zincirinde race (Docker WG'den önce kalkar) | Orta | Orta | systemd bağımlılıkları + After= kuralları (16) | | Onaysız update zinciri bypass eder | Düşük | Yüksek | K-11 kilitleri + Semaphore'da onay adımı (10) | ## 9. Uygulama Planı 1. Merkez VDS: WireGuard hub → hbbs/hbbr → MeshCentral → Semaphore → Prometheus/Grafana/Kuma sırasıyla kurulur. 2. Saha imajı: Ubuntu Server → GNOME → Docker → WireGuard spoke → RustDesk/MeshCentral agent → Node Exporter. 3. İlk 2 LAB cihazı bu mimariye göre uca eklenir, 3 akış (erişim/boot/update) testlenir. ```bash # örnek: cihaz kimliği standardı (K-12) hostnamectl set-hostname bf-12010193 ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | 3 Mermaid'deki her ok için bağlantı testi | Uçtan uca akış çalışır | LAB(2) | | VDS down simülasyonu (hbbs kapalı) | SSH-over-WG + MeshCentral ayakta | P1(5) | | Güç kesintisi simülasyonu | Boot zinciri sırayla toparlanır | LAB(2) | ## 11. Rollback Mimari değişikliği 24-DECISION-LOG PR'ı ile yapılır; bu dosyanın diyagramları aynı PR'da güncellenir. Yanlış bileşen seçimi sahaya yayılmadan LAB'da geri alınır (10-UPDATE rollback zinciri). ## 12. Kontrol Listesi - [ ] 3 Mermaid bloğu render oluyor (Docusaurus + GitHub preview). - [ ] Her bileşen adının kararı 24'te var (K-01…K-14). - [ ] `bf-` / `BF-` adlandırması diyagramlarda tutarlı. ## 13. Açık Sorular - [ ] Merkez servislerin tek VDS'e sığıp sığmadığı — LAB yük testi (sahibi: 25-ROADMAP). - [ ] Prometheus federasyonu gerekip gerekmediği — 700 node ölçümü (sahibi: 14-MONITORING). --- ## Ek: Mermaid — 1. Erişim ve yönetim akışı (Internet → WireGuard → SSH/RDP/Mgmt) ```mermaid flowchart LR subgraph SAHA["Saha (bf-)"] PC["Ubuntu Server 26.04.1
GNOME + Docker/MEG"] end subgraph MERKEZ["Merkez VDS"] WG["WireGuard hub"] SEM["Ansible/Semaphore"] MON["Prometheus/Grafana
Uptime Kuma"] RD["RustDesk hbbs/hbbr"] MC["MeshCentral"] end PC -->|Internet: WG tüneli
PersistentKeepalive=25| WG WG --> SEM WG --> MON WG -->|SSH 22, RDP 3389
yalnızca tünel içi| PC PC -->|Internet doğrudan
VDS genel adresi| RD PC -->|Internet doğrudan
VDS genel adresi| MC RD -.->|WG çökse bile
ayakta| PC MC -.->|WG çökse bile
ayakta| PC ``` ## Ek: Mermaid — 2. Power boot zinciri ```mermaid flowchart TB PWR["Power ON
(BIOS: Restore on AC Power Loss = Power On)"] --> BIOS["BIOS/UEFI"] BIOS --> UB["Ubuntu Server 26.04.1
multi-user terminal boot"] UB --> NET["Network
(NetworkManager/systemd-networkd)"] NET --> WGV["WireGuard
(wg-quick@wg0 enable)"] WGV --> DKR["Docker daemon
(log rotasyonlu)"] DKR --> MEG["MEG konteyneri
(unless-stopped)"] MEG --> BF["Blueforce servisleri"] BF --> REM["Remote: SSH + xRDP +
RustDesk + MeshCentral agent"] REM --> MON["Monitoring:
Node Exporter + Uptime Push"] ``` ## Ek: Mermaid — 3. Update onay akışı ```mermaid flowchart LR F["Bulundu
(güvenlik bülteni / MEG sürümü)"] --> R["Rapor
(etki + geri alma planı)"] R --> O{"Onay
(merkezi admin)"} O -->|Red| F O -->|Onay| LAB["LAB 2 cihaz"] LAB -->|Başarılı| P1["PILOT-1 5 cihaz"] P1 -->|Başarılı| P2["PILOT-2 20 cihaz"] P2 -->|Başarılı| W1["WAVE-1 50 cihaz"] W1 -->|Başarılı| W2["WAVE-2 100 cihaz"] W2 -->|Başarılı| PROD["PRODUCTION
kalan ~523 cihaz"] LAB -->|Başarısız| RB["Rollback
(10-UPDATE zinciri)"] P1 -->|Başarısız| RB P2 -->|Başarısız| RB W1 -->|Başarısız| RB W2 -->|Başarısız| RB ```
# 02 — Cihaz Adlandırma ve Envanter > Kısa özet: 8 haneli bayi numarasından türetilen tek cihaz kimliği (`BF-`/`bf-`) standardı ve 9 sistemde kullanımı; donanım taraması yapan `bf-hardware-inventory.sh` sözleşmesi. Filoda join anahtarıdır. - Dosya: `docs/02-DEVICE-NAMING-AND-INVENTORY.md` - İlgili kararlar: `24-DECISION-LOG.md#K-12` (cihaz kimliği), `K-09` (envanter önce, imaj sonra) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazda tek, okunabilir, çakışmasız kimlik standardı kurmak ve bu kimliğin 9 sistemde birebir aynı değeri taşımasını sağlamak. Bayi numarası değişirse ne yapılacağı da bu dosyanın prosedürüdür. ## 2. Kapsam - Kapsam içi: `BF-`/`bf-` formatı, doğrulama kuralı, 9 sistemdeki kullanım tablosu, `bf-hardware-inventory.sh` JSON+Markdown sözleşmesi, yeniden-numaralandırma prosedürü. - Kapsam dışı: hostname atama komut akışı detayı (05-ONE-CLICK-INSTALLER), Ansible inventory şeması detayı (09-FLEET), monitoring etiket şeması detayı (14-MONITORING). ## 3. Kararlar ```text KARAR: 8 haneli bayi no → Device ID `BF-` (büyük, insan arayüzü) + hostname `bf-` (küçük, makine adı); WireGuard peer adı, RustDesk etiketi, Ansible inventory adı, monitoring etiketi hepsi aynı ID'yi kullanır. GEREKÇE: Tek kimlik 9 sistemde join anahtarıdır; büyük/küçük ayrımı insan-makine karışıklığını önler (insan `BF-`, makine `bf-`). Örnek: `BF-12010193` / `bf-12010193`. ALTERNATİF: Rastgele UUID / MAC-tabanlı ad — saha teknisyeni için okunamaz, bayi eşleşmesi manuel olur → elendi. RİSK: Bayi no değişirse kimlik çakışması; azaltma: §9'daki yeniden-no prosedürü + envanter benzersizlik kontrolü. MALİYET: Ücretsiz. LİSANS: Yok (isimlendirme standardı). ``` ```text KARAR: Donanım taraması `bf-hardware-inventory.sh` ile yapılır; çıktı hem JSON (makine) hem Markdown (insan) üretir; zorunlu alanlar §9'daki sözleşmededir. GEREKÇE: Heterojen saha donanımı bilinmeden golden image dondurulamaz (K-09); tarama LAB/PİLOT öncesi her profili belgeler, imaj test matrisine girer. ALTERNATİF: Manuel envanter tablosu — 700 cihazda eksik/hatalı veri riski nedeniyle elendi. RİSK: Script farklı donanımda alan boş bırakır; azaltma: §10'daki şema doğrulama testi. MALİYET: Ücretsiz. LİSANS: Yok (şirket içi script). ``` ## 4. Neden Bu Karar? Bayi numarası sahada zaten bilinen tek anahtardır; yeni bir numara üretmek eşleşme maliyeti doğurur. 8 hane sabit uzunluk (`^[0-9]{8}$`) yazım hatasını format düzeyinde yakalar. 9 sistemde aynı adın kullanılması, arıza anında "hangi cihaz?" sorusunu tek cevaba indirir: `BF-12010193` denen cihaz her panelde aynı cihazdır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | UUID | Çakışmasız | Okunamaz, bayi eşleşmesi manuel | Elendi | | MAC-tabanlı ad | Donanıma bağlı benzersiz | Ağ kartı değişince ad değişir, okunamaz | Elendi | | Seri-no tabanlı ad | Üretici garantili | Format üreticiye göre değişir, 9 sistemde normalize gerekir | Elendi | ## 6. Avantajlar - Tek format kuralı (`^[0-9]{8}$`) tüm giriş noktalarında (kurucu, Ansible, envanter) aynı regex ile doğrulanır. - Büyük/küçük ayrımı log aramalarında ve etiketlerde karışıklığı önler. - Envanter JSON'u monitoring ve Ansible'a doğrudan beslenebilir. ## 7. Dezavantajlar - Bayi no değişimi 9 sistemde atomik güncelleme gerektirir (prosedür §9). - 8 hane dışı gerçek no'lar (kısa/uzun) sıfır-dolgu kuralına ihtiyaç duyar — kural: soldan sıfırla 8 haneye tamamlanır. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Yinelenen bayi no (çift kayıt) | Düşük | Yüksek | Merkezi envanterde benzersizlik kontrolü, kurucu kaydı reddeder | | Bayi no değişiminde eski adın artıkları | Orta | Orta | Yeniden-no prosedürü (§9) + eski adın 30 gün alias tutulması | | Envanter JSON şema kayması | Düşük | Orta | Şema sürümü (`schema_version`) + CI doğrulama | ## 9. Uygulama Planı 1. Bayi no doğrulama kuralı: `^[0-9]{8}$` — uymayan girdi reddedilir, kurucu devam etmez (05'teki akış). 2. 9 sistemde kimlik kullanımı: | # | Sistem | Kullanılan değer | Örnek | |---|---|---|---| | 1 | DNS / hostname | `bf-` | `bf-12010193` | | 2 | WireGuard peer adı | `bf-` | `bf-12010193` | | 3 | RustDesk cihaz etiketi | `BF-` | `BF-12010193` | | 4 | MeshCentral cihaz adı | `BF-` | `BF-12010193` | | 5 | Ansible inventory adı | `bf-` | `bf-12010193` | | 6 | Prometheus/Grafana etiketi (`device_id`) | `BF-` | `BF-12010193` | | 7 | Log alanı (`device_id`) | `BF-` | `BF-12010193` | | 8 | Merkezi envanter kaydı (anahtar) | `BF-` | `BF-12010193` | | 9 | Runbook/arıza kaydı başlığı | `BF-` | `BF-12010193` | 3. `bf-hardware-inventory.sh` sözleşmesi — çalıştırma ve çıktılar: ```bash # saha PC'sinde (kurucu öncesi veya bağımsız tarama) sudo ./bf-hardware-inventory.sh --dealer-id 12010193 --out-dir /var/lib/blueforce/inventory # çıktılar: # /var/lib/blueforce/inventory/BF-12010193.json # /var/lib/blueforce/inventory/BF-12010193.md ``` JSON zorunlu alanları (`schema_version: 1`): ```json { "schema_version": 1, "device_id": "BF-12010193", "hostname": "bf-12010193", "dealer_id": "12010193", "collected_at": "2026-09-15T10:00:00+03:00", "cpu": {"model": "...", "cores": 4, "arch": "x86_64"}, "memory_mb": 8192, "disks": [{"name": "sda", "size_gb": 256, "type": "ssd"}], "network": {"macs": ["..."], "wireless": true}, "gpu": {"model": "..."}, "bios": {"vendor": "...", "version": "...", "ac_restore": "Power On"}, "os": {"distro": "Ubuntu", "version": "26.04.1"}, "notes": "" } ``` Markdown çıktısı aynı alanların insan-okunur tablosudur (başlık: `# Donanım Envanteri — BF-12010193`, her bölüm tablo satırı). 4. Yeniden-numaralandırma prosedürü (bayi no değişirse): 1. Eski `BF-` kaydı envanterde `retired` işaretlenir, 30 gün alias tutulur. 2. Yeni no ile kurucu yeniden koşar (`blueforce-install.sh --dealer-id `). 3. WireGuard peer, RustDesk etiketi, MeshCentral adı, Ansible inventory, monitoring etiketi yeni ID'ye taşınır; eski peer anahtarı iptal edilir. 4. Alias süresi dolunca eski kayıt arşivlenir. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `^[0-9]{8}$` dışı girdi (7/9 hane, harf) | Kurucu reddeder, exit ≠ 0 | LAB(2) | | JSON şema doğrulama (`schema_version`, zorunlu alanlar) | Eksik alan = hata | LAB(2) | | Yinelenen no ile kayıt | Merkezi envanter reddeder | LAB(2) | | 9 sistemde ad tutarlılığı | Tümü aynı no'yu taşır | P1(5) | ## 11. Rollback 1. Yanlış no ile kurulan cihaz: `blueforce-install.sh --dealer-id ` idempotent yeniden koşar, hostname + peer + etiketler düzelir. 2. Envanter kaydı hatalıysa JSON yeniden üretilir, merkezi kayıt güncellenir; eski sürüm arşivde tutulur. ## 12. Kontrol Listesi - [ ] Format regex'i kurucu + envanter + Ansible'da aynı (`^[0-9]{8}$`). - [ ] JSON zorunlu alan listesi script ile eşleşiyor. - [ ] 9 sistem tablosundaki her satırın sahibi doküman linkli (06/07/08/09/14/15/20). - [ ] Yeniden-no prosedürü runbook'a işlendi. ## 13. Açık Sorular - [ ] 8 haneden kısa/uzun gerçek no var mı — saha listesiyle doğrulanacak (sahibi: 18-ROLLOUT). - [ ] Merkezi envanterin fiziksel konumu (VDS'de dosya mı, Semaphore Key Store mu) (sahibi: 09-FLEET yazarı). --- ## Ek: Kimlik Akışı ```mermaid flowchart LR BAYI["Bayi no
12010193"] -->|^[0-9]8$| KURUCU["blueforce-install.sh"] KURUCU --> H["hostname
bf-12010193"] KURUCU --> WG["WireGuard peer
bf-12010193"] KURUCU --> ENV["Envanter JSON/MD
BF-12010193"] ENV --> ANS["Ansible inventory"] ENV --> MON["Prometheus etiketi"] ENV --> RD["RustDesk / MeshCentral"] ```
# 03 — Ubuntu Baseline > Kısa özet: Saha PC'lerinin standart zemini: Ubuntu Server 26.04.1+ LTS minimal kurulum, GNOME sonradan eklenir, terminale boot edilir, `unattended-upgrades` kapalıdır. BIOS güç-kurtarma ayarı ve GUI aç/kapa anahtarları bu dosyadadır. - Dosya: `docs/03-UBUNTU-BASELINE.md` - İlgili kararlar: `24-DECISION-LOG.md#K-01` (OS/sürüm), `K-02` (GNOME), `K-03` (boot + bf-gui), `K-11` (otomatik update kapalı) - Durum: [ ] Taslak --- ## 1. Amaç Her saha PC'sinin aynı minimal zeminden başlamasını garanti etmek: hangi ISO, hangi masaüstü, hangi boot hedefi, hangi servisler açık/kapalı. Bu zemin 04'teki imajın ve 05'teki kurucunun varsayımıdır. ## 2. Kapsam - Kapsam içi: ISO seçimi, BIOS güç-kurtarma ayarı, boot hedefi (multi-user), GUI katmanı (GNOME + xRDP + `bf-gui-*`), `unattended-upgrades` kapatma, açılışta koşan servis listesi. - Kapsam dışı: imaj üretim yöntemi (04), kurucu modül detayları (05), WireGuard/SSH sertleştirme (06/08/11). ## 3. Kararlar ```text KARAR: Saha standardı Ubuntu Server 26.04.1+ LTS'tir (GUI'siz minimal kurulum; GNOME sonradan eklenir). GEREKÇE: 26.04.1 LTS resmi indirme sayfasında yayında (kod adı Resolute Raccoon); Server ISO'su varsayılan olarak GUI kurmaz, minimal zemin + ihtiyaca göre GUI standart yoldur. Noktasürüm (.1+) baz alınır çünkü ilk .0 dalgasındaki kurulumcu hataları genelde .1'de toplanır. ALTERNATİF: Ubuntu 24.04 LTS — 26.04 doğrulanamasaydı yedekti; doğrulandığı için yedekte bekler. Ubuntu Desktop 26.04 (6 GB RAM şartı) saha bütçesini zorladığı için elendi. RİSK: 26.04.x noktasürümlerinde donanım sürücü farkı; azaltma: golden image LAB(2)'de her saha donanım profilinde test edilir. MALİYET: Ücretsiz. LİSANS: Ubuntu LTS ücretsiz (ESM kapsamına bel bağlanmaz) — https://releases.ubuntu.com/ (26.04.1, doğrulanma: 2026-09-15) + https://ubuntu.com/download/desktop (sistem gereksinimi, doğrulanma: 2026-09-15). ``` ```text KARAR: Saha masaüstü ortamı GNOME'dur (Ubuntu Desktop standardı); XFCE elendi. GEREKÇE: Kullanıcı kararı + Ubuntu Desktop standardı: tanıdık modern masaüstü, teknisyen akışıyla birebir uyum. ALTERNATİF: XFCE — hafif ve düşük kaynak tüketimine rağmen kullanıcı kararıyla elendi; yedekte tutulmaz. RİSK: xRDP+GNOME kombinasyonu ek ayar isteyebilir (uyarı: GNOME oturumu RDP'de ek yapılandırma gerektirebilir); azaltma: 13-MEG-LINUX-ACCEPTANCE testi GNOME üzerinde koşar, `bf-gui-*` LAB'da 26.04.1 imajında doğrulanmadan dondurulmaz. MALİYET: Ücretsiz. LİSANS: GNOME (GPL/LGPL bileşenler), ücretsiz — https://www.gnome.org/ (doğrulanma: 2026-09-15). ``` ```text KARAR: Cihazlar terminale (multi-user) boot eder; grafik katman `bf-gui-on` / `bf-gui-off` ile systemd üzerinden açılıp kapatılır. GEREKÇE: xRDP mimarisi oturum yöneticisi (sesman) + systemd servis birimleri (`xrdp`, `xrdp-sesman`) üzerine kuruludur; GUI, display-manager + xRDP servisleri üzerinden yönetilebilir bir katman olarak modellenir. Varsayılan kapalı GUI = düşük RAM, düşük saldırı yüzeyi, öngörülebilir boot. ALTERNATİF: Her zaman grafik boot — kaynak israfı ve 700 cihazda gereksiz hata yüzeyi nedeniyle elendi. RİSK: Kesin birim adları ve GNOME oturum başlatma satırı 26.04.1'e göre değişebilir; azaltma: LAB'da 26.04.1 imajında doğrulanmadan `bf-gui-*` dondurulmaz. MALİYET: Ücretsiz. LİSANS: xRDP Apache-2.0, ücretsiz — https://github.com/neutrinolabs/xrdp (v0.10.6.1, 2026-07-07; doğrulanma: 2026-09-15). ``` ```text KARAR: Golden image'da `unattended-upgrades` kapatılır (`/etc/apt/apt.conf.d/20auto-upgrades` değerleri `0` + `apt-daily*.timer` maskeleme). GEREKÇE: `unattended-upgrades` Ubuntu Server'da varsayılan kurulu ve etkindir, günde bir kez çalışır; kapatılmazsa 700 cihaz ilk açılışta güncellemeye kalkar ("onaysız update yasak" ilkesi delinir). ALTERNATİF: Otomatik security update'e izin verme — eşzamanlı indirme + onaysız değişim riski nedeniyle elendi. RİSK: Kapatma adımı imajda atlanırsa filo kendiliğinden güncellenir; azaltma: kurucu final-check + monitoring metriği. MALİYET: Ücretsiz. LİSANS: Yok (OS yapılandırması) — https://ubuntu.com/server/docs/how-to/software/automatic-updates/ (doğrulanma: 2026-09-15). ``` ## 4. Neden Bu Karar? Server minimal kurulum + sonradan GNOME (Ubuntu Desktop standardı, kullanıcı kararı). Terminale boot, 700 cihazın varsayılan durumunu en öngörülebilir hale getirir; grafik yalnızca bakım penceresinde açılır. Otomatik update'in kapatılması, update zincirinin (10-UPDATE) "onay olmadan değişim yok" ilkesinin teknik kilididir. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Ubuntu 24.04 LTS | Olgun donanım desteği | Daha kısa destek penceresi | Yedek | | Ubuntu Desktop 26.04 | Tek adımda GUI | 6 GB RAM şartı, şişkin zemin | Elendi | | XFCE | Hafif, düşük kaynak | Kullanıcı kararıyla elendi (saha standardı GNOME) | Elendi | | Her zaman grafik boot | Teknisyen rahatlığı | Kaynak israfı, geniş hata yüzeyi | Elendi | ## 6. Avantajlar - Minimal zemin = küçük imaj, hızlı kurulum, az saldırı yüzeyi. - Kapalı GUI varsayılanı = düşük RAM + öngörülebilir boot 700 cihazda çarpan etkisi yapar. - Kapalı otomatik update = filo sürüm determinizmi. ## 7. Dezavantajlar - GNOME oturum başlatma satırı ve display-manager birim adları 26.04.1'e göre LAB'da doğrulanmadan dondurulamaz. - Teknisyen grafik istediğinde ek komut (`bf-gui-on`) gerekir — runbook'a işlenir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | 26.04.x sürücü farkı heterojen donanımda | Orta | Orta | LAB profil testi (04) | | `bf-gui-*` birim adları 26.04.1'de farklı | Orta | Düşük | LAB doğrulaması, §13 açık soru | | `unattended-upgrades` açık unutulur | Orta | Yüksek | final-check + monitoring metriği | ## 9. Uygulama Planı 1. BIOS ayarı (her saha PC'sinde, elektrik kesintisi sonrası otomatik açılış için): - `Restore on AC Power Loss = Power On` (üretici menüsünde `After Power Loss: Power On` / `AC Recovery: Power On` adlarıyla da geçer; teknisyen imaj öncesi menüden doğrular). 2. Ubuntu Server 26.04.1+ minimal kurulum (GUI seçilmez). 3. Boot hedefi terminale sabitlenir: ```bash sudo systemctl set-default multi-user.target systemctl get-default # multi-user.target beklenir ``` 4. GNOME + xRDP katmanı kurulur (kurucu 05 üzerinden): ```bash sudo apt install -y ubuntu-desktop-minimal xrdp sudo systemctl enable xrdp xrdp-sesman # GNOME oturumu varsayılan yapılır (LAB'da 26.04.1'e göre doğrulanacak satır): echo "gnome-session" > ~/.xsession ``` 5. GUI aç/kapa anahtarları (kurucu tarafından `/usr/local/bin/` altına yazılır): ```bash sudo bf-gui-on # display-manager + xrdp/xrdp-sesman başlatılır, RDP oturumu açılır sudo bf-gui-off # grafik katman durdurulur, cihaz terminale döner ``` 6. Otomatik update kapatılır: ```bash # /etc/apt/apt.conf.d/20auto-upgrades içinde her iki değer 0: # APT::Periodic::Update-Package-Lists "0"; # APT::Periodic::Unattended-Upgrade "0"; sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer ``` 7. Açılışta koşması beklenen servisler (final-check bunları denetler): `NetworkManager` (veya `systemd-networkd`), `wg-quick@wg0`, `docker`, `xrdp`+`xrdp-sesman` (yalnızca GUI-açık modda), `node_exporter`, RustDesk/MeshCentral agent, `bf-health-push.timer`. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `get-default` | `multi-user.target` | LAB(2) | | Güç kesintisi simülasyonu | BIOS ayarı ile cihaz kendiliğinden açılır | LAB(2) | | `bf-gui-on/off` sonrası RDP | GNOME oturumu açılır/kapanır | LAB(2) | | `20auto-upgrades` değerleri `0`, timer'lar maskeli | Otomatik apt çalışmaz | LAB(2) imaj denetimi | ## 11. Rollback 1. Yanlış boot hedefi: `sudo systemctl set-default multi-user.target` ile geri alınır. 2. Bozuk GNOME/xRDP katmanı: `bf-gui-off` + paket yeniden kurulumu; son çare imajdan yeniden kurulum (04). 3. Otomatik update yanlışlıkla açıldıysa §9 adım 6 yeniden uygulanır, `apt-daily*` timer durumu denetlenir. ## 12. Kontrol Listesi - [ ] ISO sürümü 26.04.1+ (LAB'da `lsb_release -a` çıktısı kayıtlı). - [ ] BIOS `Restore on AC Power Loss = Power On` her profilde doğrulandı. - [ ] `get-default` = `multi-user.target`. - [ ] `bf-gui-on/off` LAB 26.04.1 imajında test edildi. - [ ] `unattended-upgrades` kapalı + timer'lar maskeli. ## 13. Açık Sorular - [ ] GNOME oturum başlatma satırı + `bf-gui-*` kesin birim listesi — LAB 26.04.1 imajında (sahibi: Faz 3). - [ ] `NetworkManager` vs `systemd-networkd` seçimi — netplan profiliyle birlikte LAB'da netleşecek (sahibi: Faz 3, 08 yazarı). --- ## Ek: Zemin Katmanları ```mermaid flowchart TB ISO["Ubuntu Server 26.04.1+ ISO
(GUI'siz minimal)"] --> BOOT["Boot hedefi: multi-user.target
(terminale boot)"] BOOT --> GUI["GUI katmanı (kapalı varsayılan)
GNOME + xRDP + bf-gui-on/off"] GUI --> KILIT["Update kilidi
unattended-upgrades kapalı + timer maskeli"] KILIT --> SERVIS["Açılış servisleri
wg-quick + docker + node_exporter + agent'lar"] ```
# 04 — Golden Image ve Provisioning > Kısa özet: Saha imajının nasıl üretildiği ve cihaza nasıl basıldığı: autoinstall-tabanlı Ubuntu zemini + ilk açılışta kimlik enjeksiyonu. Hedef akış: USB → Ubuntu → tek script → bayi no → READY. - Dosya: `docs/04-GOLDEN-IMAGE-AND-PROVISIONING.md` - İlgili kararlar: `24-DECISION-LOG.md#K-09` (golden image yöntemi), `K-01` (OS), `K-11` (update kapalı), `K-12` (kimlik imajda değil, ilk boot'ta) - Durum: [ ] Taslak --- ## 1. Amaç Heterojen 700 saha PC'sinde aynı zemini tekrarlanabilir şekilde kurmak: imajın ne içerdiği, ne içermediği (cihaz kimliği), hangi araçla üretildiği ve sahada hangi akışla basıldığı. ## 2. Kapsam - Kapsam içi: yöntem seçimi (karşılaştırma tablosu), imaj içeriği, autoinstall rolü, USB basım akışı, kimlik enjeksiyonu, LAB test matrisi. - Kapsam dışı: kurucu modül detayları (05), BIOS ayarı detayı (03), rollout lojistiği (18-RECOVERY/18-ROLLOUT). ## 3. Kararlar ```text KARAR: Golden image = Ubuntu Server 26.04.1+ minimal + `unattended-upgrades` kapalı + `blueforce-install.sh` tek kurucu (bayi no → READY); imaj, preseed/autoinstall dosyasıyla üretilir, cihaz-spesifik kimlik (`bf-`) imajda DEĞİL ilk açılışta verilir. GEREKÇE: Donanım heterojenliği bilinmediği için bit-kopya klon (Clonezilla) tek imajda kırılgandır; autoinstall + idempotent kurulum scripti her donanım profilinde aynı zemini kurar, kimlik ilk boot'ta enjekte edilir. Hedef akış: USB → Ubuntu → tek script → bayi no → READY. ALTERNATİF: Clonezilla bit-kopya (hızlı ama donanım-fragil → yedek yöntem olarak tutulur); Packer/custom ISO (ek derleme hattı yükü → elendi); cloud-init (autoinstall'a göre saha-USB akışına daha az uygun → elendi). RİSK: İlk imaj heterojen donanımda sürücü eksiltebilir; azaltma: önce `bf-hardware-inventory.sh` taraması (18-ROLLOUT), LAB'da her profilde test. MALİYET: Ücretsiz. LİSANS: Ubuntu autoinstall/subiquity (ücretsiz, Ubuntu lisans seti içinde) — https://releases.ubuntu.com/26.04/ (doğrulanma: 2026-09-15). NOT: resmi autoinstall sözdizimi LAB çıktısıyla kesinleşir. ``` ## 4. Neden Bu Karar? Bit-kopya klon, kaynak makinenin sürücü setini dondurur; farklı anakart/ağ kartı olan sahada açılmayan cihaz üretir. Autoinstall her makinede donanıma göre kurulum yapar, ardından aynı kurucu script aynı zemini kurar — sonuç donanımdan bağımsız deterministik zemin + ilk boot'ta enjekte edilen benzersiz kimliktir. Custom ISO/Packer ek derleme hattı (imzalı ISO, build sunucusu, sürüm takibi) getirir; sadelik kuralını bozar. ## 5. Alternatifler | Yöntem | Artı | Eksi | Sonuç | |---|---|---|---| | Ubuntu autoinstall (user-data) | Resmi yol, donanım-bağımsız, USB'den katılımsız | Sözdizimi LAB'da doğrulanmalı | **Seçildi** | | Clonezilla bit-kopya | Çok hızlı klon, basit | Donanım-fragil, kimlik çakışması riski | Yedek yöntem | | cloud-init (NoCloud USB) | Esnek ilk-boot yapılandırma | Saha-USB akışına autoinstall kadar uygun değil, ek katman | Elendi | | Custom ISO (cubic/live-build) | Her şey gömülü tek ISO | Derleme hattı + imza + sürüm yükü | Elendi | | Packer + QEMU build | Tekrarlanabilir build | Ek altyapı, öğrenme eğrisi, 700 PC için aşırı | Elendi | | Ansible-only (kurulu OS üstüne) | İmaja gerek yok | Temel OS kurulumu yine manuel/USB gerektirir | Tamamlayıcı (kurucu sonrası filo) | ## 6. Avantajlar - İmaj kimliksizdir: tek USB tüm sahada kullanılır, yanlış kimlikli cihaz çıkmaz. - Kurucu idempotent olduğu için yarım kalan kurulum kaldığı yerden tamamlanır. - Yedek yöntem (Clonezilla) hazırda bekler; autoinstall bir profilde takılırsa sahada blokaj olmaz. ## 7. Dezavantajlar - Autoinstall sözdizimi Faz 1'de resmi-doğrulamalı değil; LAB çıktısı beklenir. - İlk kurulum Clonezilla'ya göre daha yavaştır (paket kurulumu her cihazda tekrarlanır). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Heterojen donanımda sürücü eksikliği | Orta | Orta | Envanter taraması + LAB profil matrisi | | Autoinstall sözdizimi sürüm farkı | Orta | Düşük | LAB 26.04.1 çıktısı dondurulur, dosyası repo'da sürümlenir | | USB medyası bozuk/eskimiş | Düşük | Düşük | Çift USB + SHA256 doğrulama | ## 9. Uygulama Planı 1. İmaj içeriği (kimliksiz zemin): - Ubuntu Server 26.04.1+ minimal (GUI yok). - `unattended-upgrades` kapalı (03 §9 adım 6). - `blueforce-install.sh` + modüller USB'de `/opt/blueforce-installer/` altında (veya ilk boot'ta merkezi depodan çekilir — LAB'da netleşir). - Bootstrap SSH anahtarı (ilk Ansible run'ında rotasyon, K-13). 2. Saha akışı (hedef: teknisyen başına <15 dk aktif iş): ```text USB tak → Ubuntu Server kur (autoinstall, katılımsız) → ilk açılışta: sudo ./blueforce-install.sh --dealer-id <8hane> → script bitince: bf-status → READY → USB çıkar, cihaz sahaya alınır ``` ```bash # sahada tek komut (kimlik ilk kez burada verilir) sudo ./blueforce-install.sh --dealer-id 12010193 ``` 3. Kimlik enjeksiyonu ilk boot'ta olur: hostname + WireGuard peer + envanter kaydı kurucu tarafından yazılır; imajda hiçbir `bf-` artığı bulunmaz (final-check denetler). 4. LAB test matrisi: envanterde çıkan her donanım profili × (kurulum + güç-kesintisi + `bf-gui-on/off` + final-check) — matris dolmadan WAVE açılmaz. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Aynı USB ile 2 farklı donanım profili | İkisi de READY | LAB(2) | | İmajda kimlik artığı taraması (`bf-*`, peer key) | Hiçbir cihaz-spesifik değer yok | LAB(2) | | Yarım kesilen kurulumun tekrarı | İkinci koşu tamamlar, exit 0 | LAB(2) | | Clonezilla yedek yöntemi | Autoinstall takılan profilde READY | LAB(2) | ## 11. Rollback 1. Bozuk imaj: önceki USB sürümüne dönüş (USB'ler sürüm etiketli saklanır). 2. Kurulum sonrası hata: kurucu idempotent yeniden koşar; düzelmezse 17-RECOVERY LEVEL 9 (imajdan temiz kurulum). 3. Autoinstall dosyası hatalıysa düzeltme repo PR'ı ile sürümlenir, SHA256 yeniden yayımlanır. ## 12. Kontrol Listesi - [ ] Autoinstall dosyası repo'da sürümlü + LAB çıktısıyla doğrulanmış. - [ ] USB SHA256 değeri yayımlanmış ve sahada doğrulanıyor. - [ ] İmajda kimlik artığı yok (otomatik tarama). - [ ] Her donanım profili LAB matrisinde READY almış. - [ ] Clonezilla yedek prosedürü yazılı ve test edilmiş. ## 13. Açık Sorular - [ ] Kurucu USB'de mi taşınacak, ilk boot'ta merkezden mi çekilecek (saha interneti varsayımı) (sahibi: Faz 3). - [ ] Autoinstall kesin direktif seti — LAB 26.04.1 çıktısı (sahibi: Faz 3, bu dosyanın yazarı). - [ ] USB hazırlama standardı (Ventoy vs dd vs Balena) (sahibi: Faz 3). --- ## Ek: Provisioning Akışı ```mermaid flowchart LR USB["USB
(autoinstall + kurucu)"] --> KUR["Ubuntu Server 26.04.1
katılımsız kurulum"] KUR --> TEK["sudo ./blueforce-install.sh
--dealer-id <8hane>"] TEK --> KIMLIK["Kimlik enjeksiyonu
bf- + peer + envanter"] KIMLIK --> HAZIR["bf-status → READY"] HAZIR --> SAHA["Sahaya alım"] ```
# 05 — Tek-Tık Kurucu (One-Click Installer) > Kısa özet: Sahada tek komutla cihazı READY yapan `blueforce-install.sh`: bayi no doğrulaması, 18 modül, idempotent yeniden-koşu ve final-check kapısı. Teknisyenin çalıştırdığı tek şeydir. - Dosya: `docs/05-ONE-CLICK-INSTALLER.md` - İlgili kararlar: `24-DECISION-LOG.md#K-09` (tek kurucu), `K-11` (update kapalı), `K-12` (kimlik formatı), `K-03` (bf-gui), `K-10` (Docker disiplini) - Durum: [ ] Taslak --- ## 1. Amaç Saha teknisyeninin uzmanlık gerektirmeden cihazı standarda getirmesi: tek komut, tek girdi (bayi no), net sonuç (READY / hata + log yolu). Yarım kalan veya hatalı kurulumun güvenli tekrarı. ## 2. Kapsam - Kapsam içi: komut arayüzü, bayi-no doğrulama, 18 modül listesi ve sırası, idempotency kuralları, log/bundle çıktısı, çıkış kodları. - Kapsam dışı: modül iç kodları (repo `scripts/install/`), imaj üretim yöntemi (04), filo-side yapılandırma (09). ## 3. Kararlar ```text KARAR: Saha kurulumu tek giriş noktasıdır: `sudo ./blueforce-install.sh --dealer-id <8hane>`; bayi no `^[0-9]{8}$` ile doğrulanır, uymayan girdi kurucuyu başlatmaz. GEREKÇE: Tek komut teknisyen hatasını en aza indirir; format kapısı yanlış kimliğin 9 sisteme yayılmasını baştan engeller (K-12). ALTERNATİF: Çok scriptli kurulum (ağ.sh, gui.sh, …) — sıra hatası ve atlanan adım riski nedeniyle elendi. RİSK: Tek dosya büyüdükçe bakımı zorlaşır; azaltma: 18 bağımsız modül + ana orkestratör ayrımı. MALİYET: Ücretsiz. LİSANS: Yok (şirket içi script). ``` ```text KARAR: Kurucu 18 modülden oluşur (`01-precheck` … `18-final-check`); her modül idempotenttir — yeniden koşu güvenli ve kaldığı yerden tamamlayıcıdır. GEREKÇE: Saha elektriği/kesintisi yarım kurulum üretir; idempotency olmadan her kesinti temiz-kurulum demektir. Final-check kapısı (modül 18) exit 0 vermeden cihaz READY sayılmaz. ALTERNATİF: Tek seferlik (non-idempotent) script — kesinti durumunda belirsiz ara durum nedeniyle elendi. RİSK: Modülün idempotent yazılmaması (çift kayıt, çift key); azaltma: §10'daki çift-koşu testi her modül için zorunlu. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? 700 cihaz × teknisyen başına değişen beceri = kurulumun "tek komut + tek soru" olması şarttır. 18 modül, her kararın (update-kapalı, Docker-disiplini, GUI-anahtarı, kimlik, servisler) ayrı denetlenebilir adımdır; modül numarası log satırında görünür, arıza hangi adımda kaldıysa oradan devam edilir. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Çok scriptli kurulum | Küçük dosyalar | Sıra/atlama hatası | Elendi | | Non-idempotent tek script | Daha basit kod | Kesintide temiz-kurulum zorunluluğu | Elendi | | İnteraktif sihirbaz (çok sorulu) | Esnek | Teknisyen hatası, otomasyona kapalı | Elendi | ## 6. Avantajlar - Tek komut = eğitim maliyeti minimum, runbook tek sayfa. - Idempotency = kesinti korkusu yok, "bir daha koş" her sorunun ilk cevabı. - Final-check = READY iddiası ölçülebilir, dalga kapılarına bağlanır. ## 7. Dezavantajlar - 18 modülün her biri için idempotency testi yazma maliyeti. - Tek giriş noktası, modül-atlama (debug) ihtiyacında `--from/--only` bayraklarını zorunlu kılar. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Modül idempotent değil (çift kayıt) | Orta | Orta | Çift-koşu testi (§10) + code review kuralı | | Teknisyen yanlış no girer | Orta | Orta | Format kapısı + onay ekranı (`BF-xxxx, onaylıyor musunuz?`) | | Kurucu yarıda kesilir, cihaz ara durumda sahaya alınır | Düşük | Yüksek | Final-check exit 0 olmadan READY yok; `bf-status` bunu gösterir | ## 9. Uygulama Planı 1. Komut arayüzü: ```bash sudo ./blueforce-install.sh --dealer-id 12010193 [--from 07] [--only 18-final-check] [--check] # --check: değişiklik yapmadan denetim (final-check + modül durumları) # --from: belirtilen modülden devam et | --only: tek modül koş ``` 2. Bayi-no doğrulama: `^[0-9]{8}$`; uymazsa exit 2 + kullanım mesajı. Geçerse onay ekranı (`BF-12010193` + hostname `bf-12010193` gösterilir). 3. 18 modül (sıra sözleşmedir, yeri `scripts/install/`): | # | Modül | İşlev (dayandığı karar) | |---|---|---| | 01 | `01-precheck` | OS sürümü (26.04.1+), disk/RAM minimumu, root yetkisi, internet kontrolü | | 02 | `02-dealer-id` | No doğrulama + hostname `bf-` atama (K-12) | | 03 | `03-base-packages` | Temel paketler + `unattended-upgrades` kapatma (K-11) | | 04 | `04-gnome` | GNOME kurulumu (K-02) | | 05 | `05-xrdp` | xRDP + sesman + `bf-gui-on/off` yazımı (K-03) | | 06 | `06-ssh-hardening` | `blueforce` kullanıcısı, key-auth, root/password kapalı (K-13) | | 07 | `07-wireguard` | Spoke config, peer `bf-`, keepalive 25, `wg-quick@wg0` enable (K-05) | | 08 | `08-docker` | Docker CE + `unless-stopped` + log rotasyonu + localhost-bind (K-10) | | 09 | `09-meg` | MEG konteyneri (sabit tag, `latest` yok) (K-10) | | 10 | `10-rustdesk` | RustDesk istemci + VDS adresi + `BF-` etiketi (K-04) | | 11 | `11-meshcentral` | MeshCentral agent + `BF-` adı (K-04) | | 12 | `12-monitoring` | Node Exporter + textfile + Uptime Push (K-07) | | 13 | `13-firewall` | UFW profili (SSH/RDP yalnızca WG) (K-05) | | 14 | `14-inventory` | `bf-hardware-inventory.sh` koşusu, JSON+MD üretimi (02) | | 15 | `15-logging` | journald + Docker rotasyon + `bf-status/bf-diagnostics` (K-14) | | 16 | `16-boot` | multi-user default + BIOS notu + servis enable denetimi (K-03) | | 17 | `17-ansible-bootstrap` | Bootstrap SSH key + ilk pull/Ansible kaydı (K-06/K-13) | | 18 | `18-final-check` | Tüm kapılar: kimlik, servisler, update-kapalı, Docker politikası, READY damgası | 4. Idempotency kuralları (her modül için zorunlu): - Varlık kontrolü önce (`id blueforce`, `dpkg -l`, `systemctl is-enabled`): varsa atla, yoksa kur. - Dosya yazımları atomik (tmp + rename) ve tekrarlanabilir (aynı girdi → aynı çıktı). - Anahtar/secret üretimi bir kez (`/var/lib/blueforce/.modül-done` damgası); `--force` olmadan yeniden üretilmez. - Her modül `/var/log/blueforce-install.log` dosyasına `MODÜL-NO + OK/SKIP/FAIL` satırı yazar. 5. Çıkış kodları: `0` READY, `1` modül hatası (no logda), `2` argüman/format hatası. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Geçersiz no (7/9 hane, harf, boş) | exit 2, kurulum başlamaz | LAB(2) | | Her modülün çift-koşusu | İkinci koşu exit 0, durum değişmez (diff boş) | LAB(2) | | Elektrik-kesintisi simülasyonu (modül 09 ortasında kill) | `--from 09` ile tamamlanır, final exit 0 | LAB(2) | | `18-final-check` tüm kapıları | Eksik adımda FAIL + modül nosu, READY yok | LAB(2) | | `--check` kuru koşusu | Değişiklik yapmaz, rapor üretir | P1(5) | ## 11. Rollback 1. Modül hatası: logdaki modül nosundan `--from ` ile devam edilir. 2. Yanlış no: doğru no ile tam yeniden koşu (idempotent, kimlik modülü üzerine yazar). 3. Düzelmeyen cihaz: 17-RECOVERY LEVEL 9 (temiz imaj + kurucu). ## 12. Kontrol Listesi - [ ] 18 modül dosyası `scripts/install/` altında numaralı ve çalışır durumda. - [ ] Her modülde varlık-kontrolü + damga + log satırı var. - [ ] Format regex'i 02 ile aynı (`^[0-9]{8}$`). - [ ] `18-final-check` K-10/K-11 kapılarını denetliyor (unless-stopped, latest yok, update kapalı). - [ ] `--check/--from/--only` bayrakları çalışıyor. ## 13. Açık Sorular - [ ] Kurucu USB'de mi, merkezden `curl` ile mi alınacak (saha interneti varsayımı) (sahibi: Faz 3, 04 yazarı ile ortak). - [ ] MEG sabit tag'inin kim tarafından, hangi onay ile güncelleneceği (sahibi: 10-UPDATE / 12-DOCKER yazarları). --- ## Ek: Kurucu Akış ```mermaid flowchart TB CMD["sudo ./blueforce-install.sh
--dealer-id <8hane>"] --> FMT{"^[0-9]8$ ?"} FMT -->|Hayır| EXIT2["exit 2
kullanım mesajı"] FMT -->|Evet| M01["01-precheck … 17-bootstrap
(idempotent modüller)"] M01 --> M18["18-final-check"] M18 -->|Tümü OK| READY["READY damgası
bf-status yeşil"] M18 -->|FAIL| LOG["modül nosu + log yolu
--from ile devam"] ```
# 06 — Kullanıcılar, SSH ve Yetkiler > Kısa özet: Saha cihazlarındaki kullanıcı modeli (`blueforce` bakım kullanıcısı), SSH kimlik doğrulama mimarisi (anahtar zorunlu, password kapalı, root kapalı) ve tek private key riskine karşı per-admin anahtar + SSH CA hedefi. 700 cihazda güvenli ve iptal edilebilir erişimin sözleşmesidir. - Dosya: `docs/06-USERS-SSH-AND-PERMISSIONS.md` - İlgili kararlar: `24-DECISION-LOG.md#K-13` (SSH mimarisi), `K-05` (SSH yalnızca WireGuard arkası), `K-12` (cihaz kimliği) - Durum: [ ] Taslak --- ## 1. Amaç 700 saha cihazına kimlerin, hangi kullanıcıyla, nasıl bağlanacağını sabitlemek: tek bakım kullanıcısı, anahtar-zorunlu SSH, kapalı root/password, sızıntıda blast-radius'u sınırlayan anahtar dağıtım modeli. ## 2. Kapsam - Kapsam içi: `blueforce` kullanıcısı, servis kullanıcıları, sudo politikası, `sshd_config` kilitleri, per-admin anahtar dağıtımı (Ansible), anahtar iptal prosedürü, SSH CA hedefi. - Kapsam dışı: WireGuard tünel kurulumu (08), Ansible playbook detayları (09), UFW/firewall kuralları (11). ## 3. Kararlar ```text KARAR: Saha cihazlarında tek insan bakım kullanıcısı `blueforce`'tur; servisler ayrı systemsız-login kullanıcılarla çalışır, sudo kontrollüdür. GEREKÇE: Tek bakım kimliği = 9 sistemde (02-NAMING, WireGuard, Ansible, monitoring) join anahtarı; servislerin ayrı kullanıcıyla çalışması bir servisin ele geçirilmesinin bakım yetkisine sıçramasını engeller. ALTERNATİF: Kişi başına ayrı login — 700 cihaz × N teknisyen authorized_keys kaosu nedeniyle elendi; ayrım SSH anahtarı seviyesinde (per-admin key) yapılır, kullanıcı seviyesinde değil. RİSK: Tek kullanıcı adı hedefli saldırıya odaklanır; azaltma: password kapalı + yalnızca WireGuard arayüzünden SSH (08). MALİYET: Ücretsiz. LİSANS: OpenSSH (BSD-lisanslı), ücretsiz. ``` ```text KARAR: Root SSH girişi kapalıdır (`PermitRootLogin no`); password ile SSH kapalıdır (`PasswordAuthentication no`); anahtar zorunludur (`PubkeyAuthentication yes`). GEREKÇE: Kaba-kuvvet yüzeyi root+password ikilisinden gelir; ikisi de kapatılınca uzaktan giriş yalnızca dağıtılmış anahtarlarla mümkündür. Root yetkisi yalnızca `sudo` ile, denetim izi bırakarak alınır. ALTERNATİF: Password auth açık tutma — 700 internete çıkabilen cihazda kaba-kuvvet + zayıf parola riski nedeniyle elendi. RİSK: Anahtarını kaybeden admin erişemez; azaltma: bootstrap anahtarı + acil erişim prosedürü (§9) + RustDesk/MeshCentral yedek kanalı (07). MALİYET: Ücretsiz. LİSANS: OpenSSH (BSD-lisanslı), ücretsiz. ``` ```text KARAR: 700 cihaza tek private key YAYILMAZ — her adminin kendi anahtar çifti vardır; cihazlardaki `authorized_keys` Ansible ile merkezi dağıtılır; uzun vadede SSH CA (sertifika tabanlı giriş) hedeflenir. GEREKÇE: Tek private key'in sızıntı blast-radius'u 700 cihazdır; per-admin anahtar + merkezi dağıtım, sızıntıyı tek anahtarın iptali + tek Ansible run'ına indirger. SSH CA hedefi anahtar dağıtım gecikmesini ortadan kaldırır (kısa ömürlü sertifika). ALTERNATİF: Tek paylaşılan private key — basit ama 700 cihazlık blast-radius nedeniyle elendi. RİSK: Anahtar dağıtım gecikmesi yeni cihazda erişimsizlik; azaltma: golden image'da bootstrap anahtarı + ilk Ansible run'ında rotasyon (04-GOLDEN-IMAGE). MALİYET: Ücretsiz. LİSANS: OpenSSH (BSD-lisanslı), ücretsiz. ``` ## 4. Neden Bu Karar? Savunma katmanları üst üste biner: (1) SSH yalnızca WireGuard arayüzüne bağlıdır (08), internetten port 22 görünmez; (2) o dar kapıda bile password/root kapalıdır; (3) anahtarlar kişi bazlıdır, çalınan tek anahtar tek komutla iptal edilir. Tek paylaşılan key, 700 cihazda "bir sızıntı = filo kaybı" demektir; bu risk, per-admin anahtarın getirdiği küçük dağıtım yükünden ağır basar. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Tek paylaşılan private key | Dağıtım çok basit | 700 cihazlık blast-radius | Elendi | | Password auth açık | Acil girişte kolay | Kaba-kuvvet yüzeyi | Elendi | | Kişi başına ayrı login | Denetimde ad bazlı iz | 700 cihazda kullanıcı kaosu | Elendi | | SSH CA (hemen) | En temiz iptal/rotasyon | Kurulum + Faz 1 yükü | Hedef (ileride) | ## 6. Avantajlar - Sızıntıda iptal tek Ansible run'ı ile filoya yayılır. - Root'un doğrudan girişi olmadığı için tüm yetkili işlem `sudo` log'unda iz bırakır. - Servis/servis-dışı ayrımı, MEG veya agent zafiyetinin bakım yetkisine sıçramasını engeller. ## 7. Dezavantajlar - Yeni admin için anahtar üret + dağıt + test döngüsü gerekir (runbook'a işlenir). - SSH CA kurulana kadar `authorized_keys` dağıtım gecikmesi yaşanabilir (bootstrap anahtarı ile kapatılır). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Tek private key sızıntısı (yasak ihlali) | Düşük | Yüksek (700 cihaz) | Politika + denetim (§12); sızıntıda tüm filo rotasyonu (17-RECOVERY) | | Admin anahtar kaybı | Orta | Düşük | Bootstrap anahtarı kasada; acil erişim prosedürü (§9) | | `authorized_keys` senkron kayması | Orta | Orta | Ansible run sonunda `ssh -i` doğrulama testi (09) | | SSH CA'ye hiç geçilememesi | Orta | Düşük | Per-admin model CA'sız da sürdürülebilir; CA Faz-sonrası hedef | ## 9. Uygulama Planı 1. Golden image: `blueforce` kullanıcısı + bootstrap `authorized_keys` + kilitli `sshd_config` ile gelir (04). 2. Her admin kendi anahtar çiftini üretir, public key merkez envantere işlenir. 3. Ansible `users/ssh` rolü `authorized_keys`'i dağıtır, bootstrap anahtarını ilk run'da rotasyona sokar (09). 4. Sızıntı/ayrılışta ilgili public key envanterden çıkarılır, playbook tüm filoya koşar. 5. Faz-sonrası: SSH CA kurulumu değerlendirilir (CA private key merkezde, host sertifikaları ilk boot'ta). ```bash # örnek: sshd kilitleri (hedef durum) grep -E '^(PermitRootLogin|PasswordAuthentication|PubkeyAuthentication)' /etc/ssh/sshd_config # beklenen: PermitRootLogin no / PasswordAuthentication no / PubkeyAuthentication yes # örnek: acil erişim testi (merkezden, WireGuard üzerinden) ssh -i ~/.ssh/admin-emirhan -o BatchMode=yes blueforce@10.8.0.15 echo OK ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `ssh root@` | Red (Permission denied) | LAB(2) | | Parola ile giriş denemesi | Red | LAB(2) | | Geçerli admin anahtarı ile giriş | Başarılı | LAB(2) | | İptal edilen anahtar ile giriş | Red (dağıtım sonrası) | LAB(2) | | `sudo -l` çıktısı | Yalnızca tanımlı komutlar | LAB(2) | ## 11. Rollback 1. Hatalı `sshd_config` dağıtımında önceki sürüm Ansible ile geri basılır (`sshd -t` doğrulamalı). 2. Yanlışlıkla silinen geçerli anahtar, bootstrap anahtarı veya yedek kanaldan (RustDesk/MeshCentral, 07) girilip geri eklenir. 3. Son çare: cihaz 17-RECOVERY prosedürüyle yeniden imajlanır. ## 12. Kontrol Listesi - [ ] `PermitRootLogin no`, `PasswordAuthentication no` tüm saha cihazlarında doğrulandı. - [ ] Hiçbir cihazda paylaşılan/ortak private key yok (denetim scripti 11'de). - [ ] Her admin anahtarı envanterde kayıtlı, ayrılanlarınki çıkarılmış. - [ ] Bootstrap anahtarı fiziksel kasada + erişim log'lu. - [ ] SSH yalnızca WireGuard arayüzünde dinliyor (08 ile birlikte test). ## 13. Açık Sorular - [ ] SSH CA için zaman çizelgesi ve CA altyapısı (sahibi: Faz-sonrası plan, 25-ROADMAP). - [ ] `sudo` komut beyaz listesinin kesin içeriği — LAB'da bakım akışlarıyla netleşecek (sahibi: 11-SECURITY yazarı). --- ## Ek: Mermaid — anahtar yaşam döngüsü ```mermaid flowchart LR GEN["Admin anahtar üretir
(public key envantere)"] --> DIST["Ansible authorized_keys
dağıtımı"] DIST --> USE["SSH-over-WG
günlük kullanım"] USE --> REV["Ayrılış/sızıntı:
key envanterden çıkar"] REV --> REDIST["Playbook tüm filoya
yeniden koşar"] REDIST --> VER["İptal testi:
eski key reddedilir"] ```
# 07 — Uzaktan Erişim (4 Kanal) > Kısa özet: Saha cihazlarına 4 bağımsız erişim kanalı: SSH-over-WireGuard, xRDP + GNOME, RustDesk OSS self-hosted (birincil grafik), MeshCentral self-hosted (ikincil yönetim). AnyDesk mimariden çıkarılmıştır; yalnızca müşteri sahasında zaten lisanslıysa opsiyonel modül olarak kurulur. - Dosya: `docs/07-REMOTE-ACCESS.md` - İlgili kararlar: `24-DECISION-LOG.md#K-04` (4 kanal + AnyDesk hükmü), `K-03` (xRDP + bf-gui), `K-05` (WireGuard arkası), `K-02` (GNOME) - Durum: [ ] Taslak --- ## 1. Amaç WireGuard çökse bile sahaya erişimi sürdürecek yedekli uzak-erişim mimarisini sabitlemek: hangi kanal ne için kullanılır, hangisi hangi arızada ayakta kalır, AnyDesk neden yoktur. ## 2. Kapsam - Kapsam içi: 4 kanalın rolü/kurulumu/boot davranışı, WG-çöküşü analizi, AnyDesk hükmü + opsiyonel modül notu, kanal seçim matrisi. - Kapsam dışı: WireGuard tünel kurulumu (08), SSH anahtar politikası (06), filo otomasyonu (09). ## 3. Kararlar ```text KARAR: Dört erişim kanalı: (1) SSH-over-WireGuard (yönetim/otomasyon), (2) xRDP + GNOME (grafik bakım), (3) RustDesk OSS self-hosted hbbs/hbbr (birincil grafik uzak erişim, WireGuard-bağımsız), (4) MeshCentral self-hosted (ikincil yönetim: terminal/dosya/envanter, WireGuard-bağımsız). AnyDesk mimariden ÇIKARILDI. GEREKÇE: RustDesk OSS sınırsız istemci, kendi ID+röle altyapısı, E2E şifreli oturum, dosya transferi ve katılımsız erişimi ücretsiz verir; istemcide VDS genel adresi kullanıldığında WireGuard çökse bile kanal ayakta kalır. MeshCentral Apache-2.0 ile ikinci bağımsız düzlemi sağlar. AnyDesk ücretsiz sürümü yalnızca kişisel kullanımı kapsar; 700 kurumsal cihaz ticari kullanımdır ve lisanssız kullanım ihlal olur. ALTERNATİF: AnyDesk (lisans maliyeti + kapalı kaynak + self-hosted yalnızca en üst planda → elendi; yalnızca müşteri sahasında zaten lisanslıysa opsiyonel installer modülü). Tailscale/ZeroTier (freemium, merkezi kimlik → elendi). RİSK: İki VDS-bağımlı kanalın tek ortak noktası VDS + genel internet; azaltma: hbbs/hbbr + MeshCentral health monitörü (Uptime Kuma) + yedek VDS planı 17-RECOVERY'de. MALİYET: Ücretsiz (kendi VDS'i üzerinde; VDS kira bedeli hariç — altyapı maliyeti, lisans değil). LİSANS: RustDesk istemci+sunucu AGPL-3.0 (1.4.9 / 1.1.16) — https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ + https://rustdesk.com/docs/en/self-host/client-deployment/ (doğrulanma: 2026-09-15); MeshCentral Apache-2.0 (1.2.5) — https://github.com/Ylianst/MeshCentral (doğrulanma: 2026-09-15); AnyDesk hükmü — https://support.anydesk.com/docs/anydesk-licenses.md (doğrulanma: 2026-09-15) + https://anydesk.com/en/pricing. ``` ```text KARAR: xRDP grafik bakımı verir (GNOME oturumu), yalnızca WireGuard arkasında dinler; GUI varsayılan kapalıdır, bakım penceresinde `bf-gui-on` ile açılır. GEREKÇE: xRDP systemd servis birimleri (`xrdp`, `xrdp-sesman`) üzerinden yönetilebilir bir katmandır; WG-arkası dinleme RDP'yi internet taramasından gizler, kapalı GUI varsayılanı RAM'i korur. ALTERNATİF: RDP'yi doğrudan internete açma — tarama + kaba-kuvvet riski nedeniyle elendi. RİSK: Kesin birim adları ve GNOME oturum satırı 26.04.1'de değişebilir; azaltma: LAB doğrulaması olmadan `bf-gui-*` dondurulmaz (03). MALİYET: Ücretsiz. LİSANS: xRDP Apache-2.0, ücretsiz — https://github.com/neutrinolabs/xrdp (v0.10.6.1, 2026-07-07; doğrulanma: 2026-09-15). ``` ```text KARAR: Dört kanalın agent/servisleri boot'ta otomatik başlar (systemd enable): `wg-quick@wg0`, `rustdesk` katılımsız servis, MeshCentral agent, xRDP servisleri (GUI kapalı olsa da sesman hazır). GEREKÇE: Elektrik kesintisi sonrası cihaz açılınca VPN + yedek kanallar kendiliğinden gelir; 16-POWER-LOSS boot zincirinin varsayımıdır. ALTERNATİF: Manuel başlatma — 700 dokunulmaz cihazda her kesintide saha ziyareti demek → elendi. RİSK: Boot race (agent ağdan önce kalkar); azaltma: systemd `After=network-online.target` + yeniden deneme, LAB güç-kesinti testiyle doğrulanır. MALİYET: Ücretsiz. LİSANS: Yok (OS yapılandırması). ``` ## 4. Neden Bu Karar? Tek tünel körlüğü engellenir: SSH ve RDP WireGuard'a bağımlıdır (güvenlik için), RustDesk ve MeshCentral ise VDS genel adresine doğrudan çıkar (erişilebilirlik için). RustDesk birincil grafiktir çünkü katılımsız erişim + dosya transferini OSS'te verir; MeshCentral ikinci düzlemdir çünkü terminal/dosya/envanter sunar ve RustDesk arızasında devreye girer. AnyDesk'in ücretsiz katmanı ticari kullanımı kapsamadığı için 700 cihazda lisans ihlali doğurur — bu hukuki risk teknik kolaylığa ağır basar. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | AnyDesk (ücretsiz) | Tanınmış, kolay | Ticari kullanımda lisans ihlali | Elendi | | AnyDesk (lisanslı) | Kurumsal destek | 700 cihaz lisans bedeli + kapalı kaynak | Opsiyonel modül (bkz. §9) | | Tailscale/ZeroTier | Kolay mesh | Freemium, merkezi kimlik | Elendi | | Tek kanal (yalnızca SSH) | En sade | Tünel çökerse körlük | Elendi | | VNC doğrudan | Basit | Şifresiz/zayıf varyantlar, WG-dışı risk | Elendi | ## 6. Avantajlar - WireGuard çökse bile 2 kanal (RustDesk, MeshCentral) ayakta — filo kör kalmaz. - Tüm kanallar ücretsiz/self-hosted; veri üçüncü parti buluta çıkmaz. - Kanal başına rol ayrımı (otomasyon / grafik bakım / grafik erişim / envanter) runbook'ları sadeleştirir. ## 7. Dezavantajlar - 4 kanal = 4 agent/servis bakımı, 4 izleme hedefi (14-MONITORING'e yük). - RustDesk + MeshCentral aynı VDS'teyse ortak arıza noktası (yedek VDS planı gerekir). - Teknisyenin hangi kanalı ne zaman kullanacağı eğitim gerektirir (kanal matrisi §9). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | WireGuard çöküşü (istemci/saha tarafı) | Orta | Orta | RustDesk/MeshCentral bağımsız kanallar (§9 analizi) | | VDS kesintisi (hbbs+hbbr+MeshCentral) | Düşük | Yüksek | Health monitörü + yedek VDS planı (17) | | RustDesk ID çakışması / key sızıntısı | Düşük | Orta | Self-hosted key, `bf-` etiketi, key rotasyon prosedürü | | xRDP + GNOME ek ayar ihtiyacı (26.04.1) | Orta | Orta | LAB'da `bf-gui-*` doğrulaması (03) | ## 9. Uygulama Planı 1. Merkez VDS: `hbbs`/`hbbr` (`--net=host` önerilir) + MeshCentral kurulumu, health monitörü (Uptime Kuma) bağlanır. 2. Golden image: RustDesk istemcisi (self-hosted key + VDS adresi gömülü) + MeshCentral agent + xRDP kurulu gelir; dördü de systemd enable. 3. Cihaz kimliği (`bf-`, 02) RustDesk etiketi + MeshCentral adı + Ansible inventory adı olarak aynı yazılır. 4. Opsiyonel AnyDesk modülü: SADECE müşterinin sahasında geçerli ticari AnyDesk lisansı varsa, `blueforce-install.sh --with-anydesk` bayrağıyla kurulur; lisans anahtarı müşteri tarafından sağlanır, AMC lisans sağlamaz. Lisanssız cihaza kurulmaz. ```bash # örnek: kanal durum denetimi (saha cihazında) systemctl is-active wg-quick@wg0 xrdp rustdesk meshcentral 2>/dev/null ss -tlnp | grep -E ':(22|3389)' # yalnızca WG arayüzünde dinlemeli ``` ### Kanal seçim matrisi | Durum | Kullanılacak kanal | |---|---| | Normal bakım (komut/config) | SSH-over-WireGuard | | Grafik bakım (teknisyen) | xRDP + GNOME (WG üzerinden) | | WireGuard çöktü, grafik lazım | RustDesk OSS (birincil yedek) | | WireGuard + RustDesk çöktü | MeshCentral terminal/dosya | | Otomasyon (filo) | Ansible-over-SSH (09), kanal değil araç | ### WG çökerse RustDesk analizi RustDesk istemcisi tünele değil VDS genel adresine (`hbbs`/`hbbr` portları) doğrudan internetten çıkar; WireGuard arayüzü down olsa bile `eth0/wlan0` üzerinden röle/ID sunucusuna ulaşır. Bu bağımsızlığın iki önkoşulu vardır: (1) saha interneti ayaktadır (elektrik + ISS), (2) VDS ayaktadır. İkisi de varsa teknisyen RustDesk'ten girip `wg-quick@wg0`'ı onarır — yani RustDesk hem yedek kanal hem WG kurtarma aracıdır. Tek ortak arıza: saha internet kesintisi (o zaman hiçbir uzaktan kanal çalışmaz, 16/17 prosedürleri) veya VDS kesintisi (o zaman MeshCentral da gider; yedek VDS planı 17'de). ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | 4 kanalın normal durumda erişilebilirliği | Tümü OK | LAB(2) | | `wg-quick@wg0` stop → RustDesk oturumu | Bağlantı kurulur | LAB(2) | | WG down + RustDesk kapalı → MeshCentral terminal | Komut çalışır | P1(5) | | Güç kesintisi → 4 kanalın otomatik dönüşü | Tümü 5 dk içinde OK | LAB(2) | | RDP portu internetten tarama | Kapalı (yalnızca WG) | LAB(2) | ## 11. Rollback 1. Arızalı kanalın önceki agent sürümüne dönüşü 10-UPDATE dalga zinciriyle yapılır. 2. RustDesk key değişikliğinde eski key geçiş süresi boyunca tutulur, sonra iptal edilir. 3. Son çare: kanal agent'ı kaldırılıp kalan 3 kanalla işletim sürdürülür (17-RECOVERY). ## 12. Kontrol Listesi - [ ] hbbs/hbbr + MeshCentral VDS'te ayakta, Uptime Kuma izliyor. - [ ] 4 servisin tamamı boot-enable, güç-kesinti testinden geçti. - [ ] SSH/RDP yalnızca WG arayüzünde dinliyor. - [ ] RustDesk etiketleri `bf-` ile eşleşiyor. - [ ] AnyDesk hiçbir lisanssız cihaza kurulu değil. ## 13. Açık Sorular - [ ] hbbs/hbbr ile MeshCentral aynı VDS'te mi, ayrı mı — yük testiyle karar (sahibi: 25-ROADMAP). - [ ] RustDesk self-hosted key rotasyon sıklığı (sahibi: 11-SECURITY yazarı). --- ## Ek: Mermaid — kanal bağımsızlığı ```mermaid flowchart LR PC["Saha bf-"] -->|WG tüneli| WG["WireGuard hub"] WG --> SSH["SSH / Ansible"] WG --> RDP["xRDP + GNOME"] PC -->|doğrudan internet
VDS genel adresi| RD["RustDesk hbbs/hbbr"] PC -->|doğrudan internet
VDS genel adresi| MC["MeshCentral"] WG -.->|çökerse| DOWN{"WG down?"} DOWN -->|evet| RD DOWN -->|evet| MC ```
# 08 — WireGuard Ağı ve Network > Kısa özet: Hub-spoke WireGuard topolojisi: merkez hub, her saha cihazı `bf-<8hane>` peer'ı, `PersistentKeepalive = 25`, boot'ta otomatik tünel. SSH/RDP yalnızca tünel içinden erişilir; saha internetindeki Turkcell paket bitimi/dönüşü etkileri bu dosyadadır. - Dosya: `docs/08-WIREGUARD-AND-NETWORK.md` - İlgili kararlar: `24-DECISION-LOG.md#K-05` (hub-spoke + keepalive + otomatik reconnect), `K-12` (peer adı = cihaz kimliği), `K-04` (WG çökerse yedek kanallar) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazlık VPN topolojisini, peer adlandırma sözleşmesini, NAT arkasında tüneli ayakta tutan keepalive değerini ve saha interneti (Turkcell mobil veri) gerçekleriyle başa çıkma planını sabitlemek. ## 2. Kapsam - Kapsam içi: hub-spoke topoloji, peer = bayi ID eşleşmesi, keepalive, `wg-quick@wg0` otostart, subnet/IP planı, SSH/RDP'nin WG-arkası kuralı, Turkcell paket bitimi/dönüşü notu. - Kapsam dışı: SSH anahtar politikası (06), RustDesk/MeshCentral kurulumu (07), firewall detayları (11). ## 3. Kararlar ```text KARAR: Hub-spoke WireGuard: merkez sunucu hub, her saha cihazı spoke (peer adı = `bf-<8hane>`); istemcide `PersistentKeepalive = 25` + `wg-quick@wg0` systemd enable; RDP/SSH dış dünyaya kapalı, yalnızca WireGuard arkasında. GEREKÇE: Resmi QuickStart NAT arkasındaki eş için 25 sn keepalive'i "geniş güvenlik-duvarı yelpazesiyle çalışan makul aralık" olarak verir; `wg-quick@.service` Ubuntu paketinden gelir, enable = açılışta otomatik VPN. Elektrik kesintisi sonrası cihaz açılınca VPN kendiliğinden gelir. ALTERNATİF: Keepalive kapalı / agresif 10 sn — kapalı NAT eşlemesini düşürür, agresif değer saha verisi olmadan yazılmaz; PİLOT-1 kopma sayacına göre ayarlanır. RİSK: Turkcell CGNAT davranışı resmi kaynaktan doğrulanamaz (operatör bilgisi); azaltma: PİLOT-1'de 7/24 kopma sayacı, aralık saha verisiyle ayarlanır. MALİYET: Ücretsiz. LİSANS: WireGuard OSS (Ubuntu paketi `wireguard-tools 1.0.20250521-1ubuntu1`) — https://www.wireguard.com/quickstart/ + https://packages.ubuntu.com/resolute/wireguard-tools (doğrulanma: 2026-09-15). ``` ```text KARAR: Her WireGuard peer adı cihaz kimliğine eşittir (`bf-<8hane>` = hostname = Ansible inventory adı = monitoring etiketi); hub tarafında peer başına sabit tünel IP'si atanır. GEREKÇE: Tek kimlik (K-12) 9 sistemde join anahtarıdır; peer adı farklı olsaydı WG log'undan cihaz bulmak manuel eşleştirme gerektirirdi. Sabit IP = envanterde `bf- → 10.x` haritası, runbook ve monitoring sorguları deterministik olur. ALTERNATİF: Rastgele peer adı / dinamik IP — okunamaz, izlenemez → elendi. RİSK: Bayi no değişirse peer yeniden adlandırılmalı; azaltma: 02-NAMING yeniden-no prosedürü. MALİYET: Ücretsiz. LİSANS: Yok (isimlendirme standardı). ``` ## 4. Neden Bu Karar? Saha cihazları NAT/CGNAT arkasındadır; merkez onlara ulaşamaz, onlar merkeze çıkar. Hub-spoke bu gerçeğe uyar: spoke keepalive ile NAT eşlemesini canlı tutar, hub yalnızca dinler. 25 sn, resmi dokümanın "makul aralık" değeridir — saha ölçümü olmadan daha agresif yazmak veri israfı, kapatmak kopma demektir. Peer = bayi ID kuralı, 700 peer'lı hub config'ini insan tarafından okunabilir tutar. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Keepalive kapalı | Az trafik | NAT eşlemesi düşer, tünel ölür | Elendi | | Keepalive 10 sn | Daha canlı eşleme | Saha verisi olmadan veri israfı | Yedek (PİLOT-1 verisine göre) | | Full-mesh | Spoke'lar arası doğrudan | 700 cihazda key/config patlaması | Elendi | | Tailscale/ZeroTier | Kolay | Freemium, merkezi kimlik | Elendi (K-04) | ## 6. Avantajlar - Kernel-seviyesi VPN: düşük CPU, düşük gecikme, mini PC dostu. - Otostart: elektrik kesintisi sonrası teknisyen müdahalesi gerekmez. - Sabit IP + peer adı = monitoring ve Ansible'da doğrudan adresleme. ## 7. Dezavantajlar - Hub tek darboğaz: 700 eşin handshake + trafiği tek VDS'ten geçer (yük testi gerekir). - CGNAT davranışları (özellikle mobil) resmi kaynaktan doğrulanamaz; ayar saha verisine bağımlıdır. - Peer ekleme/çıkarma merkez config'inde yapılır; otomasyonsuz 700 peer yönetilemez (09'a bağımlılık). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Turkcell CGNAT NAT eşlemesini erken düşürür | Orta | Orta | PİLOT-1 7/24 kopma sayacı; aralık saha verisiyle ayarlanır | | Turkcell paket bitimi → veri durur | Orta | Yüksek | Paket izleme + bitim alarmı (bkz. §9); kota-dostu keepalive | | Hub VDS darboğazı (700 eş) | Orta | Yüksek | Yük testi + peer başına trafik limiti; gerekirse 2. hub | | Peer private key sızıntısı | Düşük | Orta | Peer bazlı iptal (tek peer silinir, filo etkilenmez) | ## 9. Uygulama Planı 1. Merkez hub kurulur, subnet planı dondurulur (ör. `10.8.0.0/16`, hub `10.8.0.1`, cihazlara sabit `/32`). 2. Her cihaz için peer üretilir: adı `bf-<8hane>`, tünel IP'si envanter haritasından. 3. İstemci config'i (`/etc/wireguard/wg0.conf`): hub endpoint + `PersistentKeepalive = 25` + `wg-quick@wg0 enable`. 4. Peer ekleme/çıkarma Ansible rolüyle yapılır (09); hub config'i Git'te sürümlenir. 5. PİLOT-1'de kopma sayacı koşar (handshake yaşı metriği 14'te), keepalive saha verisiyle ayarlanır. ```bash # örnek: istemci konfigürasyonu (değerler envanterden gelir) # /etc/wireguard/wg0.conf # [Interface] PrivateKey = / Address = 10.8.0.15/32 # [Peer] PublicKey = / Endpoint = :51820 # PersistentKeepalive = 25 / AllowedIPs = 10.8.0.0/16 sudo systemctl enable --now wg-quick@wg0 sudo wg show wg0 latest-handshakes # kopma sayacı girdisi ``` ### Turkcell paket bitimi / dönüşü notu Saha bağlantısı Turkcell mobil veri ise iki gerçek vardır: (1) hat CGNAT arkasındadır — merkez cihaza ulaşamaz, keepalive'lı giden tünel zorunludur; (2) kota bitiminde veri durur veya hız düşer — tünel sessizce ölür, cihaz "çevrimdışı" görünür ama arıza değildir. Önlemler: kota-dostu keepalive (25 sn varsayılan, PİLOT verisiyle ayarlanır; gereksiz agresif değer kotayı eritir), paket bitimine Uptime Kuma "last seen" alarmı + sorumlu bayiye bildirim akışı (14), kota dönüşünde tünelin kendiliğinden toparlanması (`wg-quick` + keepalive yeniden handshake eder, ek işlem gerekmez). Paket yenileme/dönüş prosedürü (hat sahibi, yenileme adımı) 18-ROLLOUT runbook'una yazılır — bu dosya yalnızca teknik davranışı sabitler. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Boot sonrası `wg show` handshake | 3 dk içinde güncel handshake | LAB(2) | | NAT arkasında 24 saat tünel | Kopma yok (sayaç temiz) | P1(5) | | Kota simülasyonu (veri kes) → geri ver | Tünel kendiliğinden toparlanır | LAB(2) | | SSH/RDP internetten tarama | Kapalı; yalnızca `10.8.0.0/16` içinden açık | LAB(2) | | 700 eş yük projeksiyonu | Hub CPU/bant genişliği sınırlar içinde | Yük testi | ## 11. Rollback 1. Hatalı hub config'i Git'teki önceki sürüme döndürülür, `systemctl reload wg-quick@wg0`. 2. Keepalive değişikliği sorun çıkarırsa önceki değere Ansible ile toplu dönüş (10-UPDATE dalga sırası). 3. Son çare: WG down iken RustDesk/MeshCentral (07) ile cihaza girilip config elle düzeltilir. ## 12. Kontrol Listesi - [ ] Peer adları `bf-<8hane>` ile birebir, envanterle eşleşiyor. - [ ] Tüm istemcilerde `PersistentKeepalive = 25` (veya PİLOT-onaylı değer). - [ ] `wg-quick@wg0` tüm sahada enable. - [ ] SSH/RDP yalnızca WG arayüzünde dinliyor. - [ ] PİLOT-1 kopma sayacı koşuyor, sonuç 24-DECISION-LOG'a işlenecek. ## 13. Açık Sorular - [ ] Keepalive'ın PİLOT-1 verisine göre 25 sn'de kalıp kalmayacağı (sahibi: 08 yazarı, P1 sonrası). - [ ] Hub'un 700 eşi tek VDS'te taşıyıp taşımayacağı — yük testi (sahibi: 25-ROADMAP). - [ ] Turkcell kota/bitim prosedürünün işletme sahibi (sahibi: 18-ROLLOUT yazarı). --- ## Ek: Mermaid — hub-spoke + keepalive ```mermaid flowchart TB subgraph SAHA["Saha (NAT/CGNAT arkası)"] S1["bf-12010193
10.8.0.15"] S2["bf-12010194
10.8.0.16"] end HUB["WireGuard hub
10.8.0.1:51820"] S1 -->|handshake + keepalive 25sn
giden bağlantı| HUB S2 -->|handshake + keepalive 25sn
giden bağlantı| HUB HUB -->|SSH/RDP yalnızca
tünel içi| S1 ```
# 09 — Filo Yönetimi (Ansible + Semaphore) > Kısa özet: 700 cihazın otomasyonu Ansible CLI (agent'sız, SSH) ile yapılır; operatör arayüzü Semaphore UI Community'dir (self-hosted, MIT). 15 standart işlem, tek/grup/pilot/tüm-filo hedefleme ve Semaphore Community sınırları bu dosyadadır. - Dosya: `docs/09-FLEET-MANAGEMENT.md` - İlgili kararlar: `24-DECISION-LOG.md#K-06` (Ansible + Semaphore Community), `K-13` (SSH anahtar dağıtımı), `K-12` (inventory adı = `bf-`), `K-11` (dalgalı yayılım) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihaza komut/config güncellemesini güvenli, tekrarlanabilir ve onaylı yaymanın yolunu sabitlemek: hangi araç, hangi 15 standart işlem, hangi hedefleme seviyeleri, Community sürümünün sınırlarında nasıl çalışılır. ## 2. Kapsam - Kapsam içi: Ansible rolü/inventory düzeni, 15 standart işlem, hedefleme seviyeleri (tek/grup/pilot/tüm-filo), Semaphore kurulumu + Community sınırları, Semaphore-down yedeği. - Kapsam dışı: monitoring (14), update onay zinciri detayı (10), SSH anahtar politikası (06). ## 3. Kararlar ```text KARAR: Birincil filo otomasyonu Ansible CLI (SSH, agent'sız); operatör arayüzü Semaphore UI Community (self-hosted, MIT). Hiçbir playbook Pro/Enterprise özelliğine bağlanmaz. GEREKÇE: En düşük hareketli parça (SSH + YAML + tek Go binary); 700 cihazda ek agent/master/K8s maliyeti yok. Community ücretsizliği resmi fiyat sayfasıyla kanıtlı ("$0, free forever"); Pro en fazla 500 managed node desteklediği için 700 cihazda ücretli yol zaten kapalı. ALTERNATİF: AWX (K8s yükü + release'ler refactoring nedeniyle duraklatıldı → elendi); Salt (minion agent + master PKI → elendi); Rudder Community (relay/Enterprise modülleri + öğrenme eğrisi → elendi); MeshCentral (erişim aracı, filo otomasyonu değil → bu rolde elendi). RİSK: Semaphore tek nokta arızası → Ansible CLI bağımsız çalışır (UI çökse filo durmaz). Community'de OIDC/2FA/Vault yok → Semaphore yalnızca WireGuard arkasında, erişim VPN + SSH anahtarı ile; gizliler yerleşik şifreli Key Store'da. MALİYET: Ücretsiz (kendi VDS'i üzerinde). LİSANS: Ansible GPL-3.0 — https://github.com/ansible/ansible; Semaphore Community MIT — https://github.com/semaphoreui/semaphore + https://semaphoreui.com/pricing (doğrulanma: 2026-09-15). ``` ```text KARAR: Tüm filo işlemleri 15 standart işlemden biri olarak Semaphore task şablonu + Ansible playbook/rolüyle tanımlanır; serbest komut yalnızca acil kır cam prosedürüyle koşar. GEREKÇE: 15 sabit işlem = denetlenebilir, test edilebilir, rollback'i tanımlı yüzey; serbest komut 700 cihazda yazım hatasını filo arızasına çevirir. ALTERNATİF: Serbest ad-hoc komut serbestliği — insan hatası blast-radius'u nedeniyle elendi. RİSK: 16. ihtiyaçta şablon dışı kalma; azaltma: yeni işlem teklifi PR ile 15 listesine eklenir (§13). MALİYET: Ücretsiz. LİSANS: Yok (operasyon standardı). ``` ```text KARAR: Hedefleme 4 seviyelidir: tek cihaz → grup → pilot dalga → tüm filo; tüm-filoya çıkış yalnızca pilot dalga başarısından sonra, onay adımlı Semaphore pipeline ile yapılır. GEREKÇE: 10-UPDATE dalga zincirinin (LAB/P1/P2/W1/W2/PROD) filo-operasyon karşılığıdır; playbook hatası LAB(2)'de yakalanır, 700 cihaza yayılmaz. ALTERNATİF: Doğrudan tüm-filo çalıştırma — tek hatanın 700 katı etki nedeniyle elendi. RİSK: Yanlış grup etiketi yanlış hedefe koşar; azaltma: `--limit` + `--check` kuru koşu zorunlu ön adım. MALİYET: Ücretsiz. LİSANS: Yok (operasyon standardı). ``` ## 4. Neden Bu Karar? 700 cihazda ajanlı mimariler (Salt minion, Rudder agent, AWX+K8s) her cihaza bir ek yük ve merkeze bir ek servis bindirir. Ansible'da ajan yoktur: SSH varsa filo vardır. Semaphore, CLI bilgisi olmayan operatörün (saha sorumlusu) güvenli düğmelere basmasını sağlar; CLI ise Semaphore çöktüğünde bağımsız yedektir. Pro sürüm 500 node ile sınırlı olduğundan 700 cihazda zaten kullanılamaz — Community sınırı bir eksik değil, mimari sabittir. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | AWX | Güçlü UI, RBAC | K8s zorunlu, release'ler duraklatıldı | Elendi | | Salt | Ölçeklenir | Minion agent + master PKI | Elendi | | Rudder Community | Politika motoru | Relay/Enterprise modülleri, öğrenme eğrisi | Elendi | | Semaphore Pro | Ek özellikler | 500 node limiti (700'e yetmez) + ücret | Elendi | | MeshCentral (filo rolü) | Zaten kurulu | Toplu config/rollback disiplini yok | Bu rolde elendi | ## 6. Avantajlar - Agent'sız: saha imajına ek yük yok, yeni cihaz envantere girer girmez yönetilir. - Semaphore down olsa filo durmaz (Ansible CLI bağımsız). - 15 sabit işlem + 4 hedefleme seviyesi = hata yüzeyi küçük, denetim izi tam. ## 7. Dezavantajlar - Community'de OIDC/2FA/Vault yok — erişim disiplini (WG-arkası + Key Store) manuel korunmalı. - Eşzamanlı 700 SSH bağlantısı Semaphore/Ansible fork ayar + hub bant genişliği ister (yük testi §13). - Windows-odaklı teknisyen için YAML/playbook eşiği (Semaphore düğmeleriyle kapatılır). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Playbook hatası filoya yayılır | Orta | Yüksek | Dalgalı yayılım + `--check` kuru koşu + LAB zorunluluğu | | Semaphore çöker | Düşük | Orta | Ansible CLI bağımsız çalışır (K-06) | | Community'de kimlik disiplini bozulur | Orta | Yüksek | WG-arkası erişim + per-admin SSH key + Key Store; denetim (§12) | | 700 eşzamanlı SSH hub'ı yorar | Orta | Orta | Fork/batch limitleri + dalga yayılımı; yük testi | ## 9. Uygulama Planı 1. Merkez: Semaphore Community (tek binary, WG-arkası) + Ansible CLI + Git-sürümlü playbook deposu kurulur. 2. Inventory: `bf-` adları, grup etiketleri (bölge/pilot-dalga/donanım-profili), `ansible_user=blueforce`, per-admin key (06). 3. 15 standart işlem playbook/rol + Semaphore task şablonu olarak yazılır, LAB(2)'de test edilir. 4. Hedefleme boru hattı: tek → grup → pilot → tüm-filo; son adıma onay kapısı konur. 5. Semaphore-down tatbikatı: CLI ile aynı playbook'un koştuğu kanıtlanır (10-Test). ```bash # örnek: kuru koşu + sınırlı hedef (zorunlu ön adım) ansible-playbook -i inventory/prod.yml playbooks/bf-gui-off.yml --limit 'pilot1' --check ansible-playbook -i inventory/prod.yml playbooks/bf-gui-off.yml --limit 'pilot1' # örnek: tek cihaz teşhisi ansible -i inventory/prod.yml 'bf-12010193' -m ping ``` ### 15 standart işlem | # | İşlem | Playbook/rol (ad) | |---|---|---| | 1 | ping (erişilebilirlik yoklaması) | `bf-ping.yml` | | 2 | status (bf-status çıktısı toplama) | `bf-status.yml` | | 3 | diagnostics (bf-diagnostics + bundle) | `bf-diagnostics.yml` | | 4 | gui-on (grafik katmanı aç) | `bf-gui-on.yml` | | 5 | gui-off (grafik katmanı kapat) | `bf-gui-off.yml` | | 6 | reboot (kontrollü yeniden başlatma) | `bf-reboot.yml` | | 7 | shutdown (kontrollü kapatma) | `bf-shutdown.yml` | | 8 | ssh-keys (authorized_keys dağıtım/rotasyon) | `bf-ssh-keys.yml` | | 9 | wg-rekey (WireGuard peer anahtar yenileme) | `bf-wg-rekey.yml` | | 10 | docker-restart (MEG konteyner yeniden başlatma) | `bf-docker-restart.yml` | | 11 | docker-update (sabit tag'e onaylı imaj geçişi) | `bf-docker-update.yml` | | 12 | apt-update (onaylı paket güncellemesi, dalgalı) | `bf-apt-update.yml` | | 13 | config-push (dosya/şablon dağıtımı) | `bf-config-push.yml` | | 14 | log-collect (journald/destek paketi toplama) | `bf-log-collect.yml` | | 15 | decommission (envanter + peer + key temizliği) | `bf-decommission.yml` | ### Hedefleme seviyeleri | Seviye | Hedef | Onay | |---|---|---| | Tek | `bf-` (arıza/teşhis) | Gerekmez (loglanır) | | Grup | bölge / donanım profili etiketi | Grup sorumlusu | | Pilot | P1(5) → P2(20) dalgaları | Merkezi admin | | Tüm-filo | kalan cihazlar (W1/W2/PROD sırasıyla) | Merkezi admin + değişiklik kaydı (10) | ### Semaphore Community sınırları (çalışma kuralları) - OIDC/SSO yok → kullanıcılar Semaphore yerel hesabı, erişim yalnızca WireGuard arkasından. - 2FA yok → parola politikası + VPN zorunluluğu bu açığı kapatır; admin hesapları envanterde kayıtlı. - Harici Vault entegrasyonu yok → gizliler Semaphore yerleşik şifreli Key Store'da; prod secret'ları playbook'a gömülmez. - Global Runner / yüksek eşzamanlılık sınırları → 700 node yük testi yapılır; gerekirse runner sayısı + fork/batch ayarlanır (§13 açık soru). - Hiçbir iş akışı Pro özelliğine bağlanmaz (Community'de çalışmayan şablon yazılmaz). ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | 15 işlemin her biri LAB(2)'de | Başarılı + log'lu | LAB(2) | | `--check` kuru koşu, değişimsiz playbook | "changed=0" | LAB(2) | | Yanlış `--limit` denemesi | Onay kapısında durur / hedef dışı etkilenmez | LAB(2) | | Semaphore kapalı → CLI run | Playbook başarıyla koşar | LAB(2) | | Pilot dalga → tüm-filo boru hattı | Onay adımları çalışır | P1(5) | ## 11. Rollback 1. Her işlemin geri alma adımı playbook'ta tanımlıdır (config-push önceki sürümü basar, docker-update önceki tag'e döner). 2. Yayılım ortasında arıza: dalga durdurulur, etkilenmiş dalga 10-UPDATE rollback zinciriyle geri alınır. 3. Semaphore veritabanı bozulursa playbook deposu (Git) + inventory'den CLI ile işletim sürer; Semaphore yeniden kurulur. ## 12. Kontrol Listesi - [ ] 15 işlemin playbook + Semaphore şablonu LAB-onaylı. - [ ] Inventory adları `bf-`, grup etiketleri güncel. - [ ] `--check` ön adımı tüm şablonlarda zorunlu. - [ ] Semaphore yalnızca WG arkasından erişilebilir. - [ ] CLI yedeği tatbikatla kanıtlandı. - [ ] Hiçbir şablon Pro/Enterprise özelliği kullanmıyor. ## 13. Açık Sorular - [ ] Semaphore Global Runner sayısı + eşzamanlılık (fork/batch) — 700 node yük testi (sahibi: 25-ROADMAP). - [ ] 16. standart işlem teklifi süreci (PR şablonu) (sahibi: 09 yazarı). - [ ] Inventory kaynak gerçeği (statik dosya mı, API mi) (sahibi: 02-NAMING yazarı). --- ## Ek: Mermaid — dalgalı yayılım ```mermaid flowchart LR DEV["Playbook PR
(Git review)"] --> LAB["LAB 2
--check + run"] LAB --> P1["PILOT-1 5"] P1 --> P2["PILOT-2 20"] P2 --> W["W1 50 → W2 100"] W --> PROD["PROD kalan"] LAB -->|hata| FIX["Düzelt +
yeniden LAB"] ```
# 10 — Update ve Rollback Politikası > Kısa özet: Sahaya onaysız update yasaktır: `unattended-upgrades` kapalıdır, her değişiklik LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD dalgalarından ve merkezi onaydan geçer. Her dalganın geri alma planı yayından önce tanımlıdır. - Dosya: `docs/10-UPDATE-AND-ROLLBACK-POLICY.md` - İlgili kararlar: `24-DECISION-LOG.md#K-11` (otomatik update yasağı + dalgalar), `K-10` (`latest`/Watchtower yasakları), `K-09` (imaj bağı) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazda "kendiliğinden değişen hiçbir şey olmasın" ilkesini teknik ve prosedürel kilitlerle garanti etmek: otomatik updater'ların kapatılması, onay zinciri, dalga planı ve rollback sözleşmesi. ## 2. Kapsam - Kapsam içi: onaysız update yasağı, `unattended-upgrades` kapatma, dalga tanımı ve geçiş kriterleri, onay rolleri, rollback zinciri. - Kapsam dışı: playbook yazım detayları (09), imaj üretim adımları (04), Docker tag disiplini detayı (12). ## 3. Kararlar ```text KARAR: Golden image'da `unattended-upgrades` kapatılır (`/etc/apt/apt.conf.d/20auto-upgrades` değerleri `0` + `apt-daily*.timer` maskeleme); tüm güncellemeler LAB→PİLOT→WAVE→PROD onay zincirinden geçer (dalgalar: LAB(2)/P1(5)/P2(20)/W1(50)/W2(100)/PROD). GEREKÇE: `unattended-upgrades` Ubuntu Server'da varsayılan kurulu ve etkindir, günde bir kez çalışır; kapatılmazsa 700 cihaz ilk açılışta güncellemeye kalkar ("onaysız update yasak" ilkesi delinir). Kaçan timer makine açılışında hemen tetiklenir (`Persistent=true` davranışı) — saha PC'leri sık kapanan makinelerdir. ALTERNATİF: Otomatik security update'e izin verme — 700 cihazda eşzamanlı indirme + onaysız değişim riski nedeniyle elendi; security update'ler öncelik işaretli onaylı dalga ile çıkar. RİSK: Kapatma adımı imajda atlanırsa filo kendiliğinden güncellenir; azaltma: `blueforce-install.sh` final-check + monitoring'de `Unattended-Upgrade` durumu metriği. MALİYET: Ücretsiz. LİSANS: Yok (işletim sistemi yapılandırması) — https://ubuntu.com/server/docs/how-to/software/automatic-updates/ + https://manpages.ubuntu.com/manpages/resolute/en/man8/unattended-upgrade.8.html (doğrulanma: 2026-09-15). ``` ```text KARAR: Onaysız update yasaktır: sahaya inen her değişiklik (OS paketi, Docker imajı, config, agent sürümü) değişiklik kaydı + merkezi admin onayı + dalga planı olmadan uygulanmaz; `latest` etiketi ve Watchtower gibi otomatik updater'lar yasaktır. GEREKÇE: 700 ölçeğinde her "küçük varsayılan" (günlük otomatik update, hareketli etiket, otomatik imaj çekme) filo-çapı arızaya dönüşür; determinizm yalnızca sabit sürüm + onaylı yayılımla korunur. ALTERNATİF: Güvenlik güncellemelerine otomatik izin — eşzamanlı indirme fırtınası + onaysız değişim riski nedeniyle elendi; acil güvenlik yaması "hızlandırılmış dalga" prosedürüyle (aynı zincir, kısaltılmış bekleme) çıkar. RİSK: Onay kuyruğu kritik yamayı geciktirir; azaltma: security dalgasına öncelik SLA'sı (§9). MALİYET: Ücretsiz. LİSANS: Yok (politika) — Docker yasak dayanağı için 12-DOCKER ve K-10'a bakılır. ``` ```text KARAR: Rollback her yayının parçasıdır: dalgaya çıkmadan önce geri alma adımı (önceki paket sürümü / önceki imaj tag'i / önceki config) tanımlanır ve LAB'da geri dönüş testi yapılır; başarısız dalga bir sonrakine geçmez, etkilenmiş dalga geri alınır. GEREKÇE: Geri alması test edilmemiş yayın, 700 cihazda tek yönlü kapıdır; LAB'da kanıtlanan rollback, PROD'da gece yarısı kurtarma prosedürüne dönüşür. ALTERNATİF: "Gerekirse bakarız" yaklaşımı — arıza anında doğaçlama riski nedeniyle elendi. RİSK: Geri alma adımı da bozuk çıkabilir; azaltma: son çare 17-RECOVERY yeniden-imaj prosedürü. MALİYET: Ücretsiz. LİSANS: Yok (operasyon standardı). ``` ## 4. Neden Bu Karar? Üç yasak tek ilkeden doğar: `unattended-upgrades` (işletim sistemi kendi kendine değişmesin), `latest` (hangi sürümün koştuğu her cihazda aynı bilinsin), Watchtower (hiçbir daemon izinsiz imaj çekmesin). Dalgalar (2 → 5 → 20 → 50 → 100 → ~523) hatanın blast-radius'unu geometrik olarak sınırlar: LAB'da yakalanan hata 2 cihazı etkiler, PROD'a sızan hata ancak 5 dalga kapısından geçmiş demektir. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Otomatik security update | Hızlı yama | Eşzamanlı indirme + onaysız değişim | Elendi (hızlandırılmış dalga ile karşılanır) | | Doğrudan tüm-filo yayın | Hızlı | Tek hatanın 700 katı | Elendi | | Watchtower ile oto-imaj | Eforsuz güncellik | Politika ihlali + upstream arşivli | Elendi (K-10) | | `latest` etiketi | Kolay | Determinizm kaybı | Elendi (K-10) | ## 6. Avantajlar - Filo sürüm determinizmi: her cihazın ne koşturduğu envanterde bilinir. - Hata erken dalgada yakalanır; PROD'a ulaşan değişiklik 5 kez kanıtlanmıştır. - Acil yama da aynı zincirden geçer — hız, disiplinsizlikle değil kısaltılmış beklemeyle kazanılır. ## 7. Dezavantajlar - Yayın döngüsü yavaştır (özellikle PROD'a tam yayılım günler sürebilir). - Onay kuyruğu ve dalga beklemeleri operasyonel disiplin ister. - Her yayın için rollback testi yazma maliyeti. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | `unattended-upgrades` imajda açık unutulur | Orta | Yüksek | install.sh final-check + monitoring metriği (K-11) | | Onay kuyruğu kritik yamayı geciktirir | Orta | Yüksek | Security dalgasına öncelik SLA'sı (§9) | | Dalga atlama ("acil" bahanesiyle) | Orta | Yüksek | Semaphore onay kapısı + değişiklik kaydı zorunluluğu | | Rollback adımı da bozuk | Düşük | Yüksek | LAB geri-dönüş testi + 17-RECOVERY son çare | ## 9. Uygulama Planı 1. Golden image: `20auto-upgrades` değerleri `0`, `apt-daily*.timer` maskeli, final-check'te doğrulamalı (04). 2. Değişiklik kaydı açılır: kapsam (paket/imaj/config), etkilenen sürümler, geri alma adımı, dalga takvimi. 3. Merkezi admin onaylar; yayın Semaphore pipeline ile dalga sırasına girer (09). 4. Her dalgada sağlık kapısı: monitoring (14) + `bf-status` örneklemesi temizse sonraki dalga açılır. 5. Başarısız dalga: yayılım durdurulur, etkilenmiş dalga rollback zinciriyle geri alınır, kök neden kayda işlenir. ```bash # örnek: otomatik update kilit denetimi (saha cihazında) cat /etc/apt/apt.conf.d/20auto-upgrades # iki değer de 0 olmalı systemctl status apt-daily.timer apt-daily-upgrade.timer # maskeli/inactive ``` ### Dalga tanımı ve geçiş kriterleri | Dalga | Büyüklük | Geçiş kriteri (sonraki dalgaya) | |---|---|---| | LAB | 2 cihaz | Fonksiyonel test + rollback testi geçti | | P1 (PİLOT-1) | 5 cihaz | 24–48 saat sorunsuz + metrikler temiz | | P2 (PİLOT-2) | 20 cihaz | 48–72 saat sorunsuz + saha geri bildirimi | | W1 (WAVE-1) | 50 cihaz | Sağlık kapısı + onay yenileme | | W2 (WAVE-2) | 100 cihaz | Sağlık kapısı + onay yenileme | | PROD | kalan ~523 cihaz | W2 temizliği + final onay | ### Security yaması öncelik SLA'sı Kritik güvenlik bülteninde aynı zincir kısaltılmış beklemeyle koşar (LAB saatler içinde, P1 aynı gün); bekleme kısaltılır, kapılar kaldırılmaz — onay ve rollback testi security dalgasında da zorunludur. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `20auto-upgrades` değerleri + timer durumu | `0` / maskeli, 24 saatte otomatik apt yok | LAB(2) imaj denetimi | | Kuru yayın (tüm dalgalar `--check`) | Değişim öngörüsü kayıtla eşleşir | LAB(2) | | LAB rollback testi | Önceki sürüme dönüş + servis ayakta | LAB(2) | | P1 48 saat gözlem | Metrik anomalisi yok | P1(5) | | Dalga-durdurma tatbikatı | Yayılım durur, P1 geri alınır | P1(5) | ## 11. Rollback 1. Etkilenmiş dalga, değişiklik kaydındaki geri alma adımıyla önceki sürüme döndürülür (paket downgrade / önceki imaj tag'i / önceki config). 2. Geri alma sonrası sağlık kapısı (monitoring + `bf-status`) doğrulanır, kayda işlenir. 3. Geri alma da başarısızsa: cihaz 17-RECOVERY prosedürüyle önceki golden image'a döndürülür (LEVEL 9 son çare). 4. Kök neden bulunmadan yayına devam edilmez; zincir başa (LAB) sarar. ## 12. Kontrol Listesi - [ ] `unattended-upgrades` tüm sahada kapalı (monitoring metriğiyle sürekli denetim). - [ ] `latest`/`Watchtower` hiçbir saha cihazında yok (12 ile birlikte denetim). - [ ] Her yayının değişiklik kaydı + onayı + rollback adımı var. - [ ] Dalga kapıları atlanmadan işletildi (Semaphore log'uyla kanıtlı). - [ ] Security SLA'sı runbook'ta yazılı, sorumlusu belli. ## 13. Açık Sorular - [ ] Dalga bekleme sürelerinin (24–72 saat) PİLOT verisine göre kalibrasyonu (sahibi: 10 yazarı, P2 sonrası). - [ ] Paket/imaj aynası (yerel mirror) gerekip gerekmediği — 700 cihazın eşzamanlı indirme yükü (sahibi: 25-ROADMAP). --- ## Ek: Mermaid — update onay akışı ```mermaid flowchart LR F["Bulundu
(bülten / MEG sürümü)"] --> R["Kayıt
(etki + rollback planı)"] R --> O{"Onay
(merkezi admin)"} O -->|Red| F O -->|Onay| LAB["LAB 2"] LAB -->|Başarılı| P1["P1 5"] P1 -->|Başarılı| P2["P2 20"] P2 -->|Başarılı| W1["W1 50"] W1 -->|Başarılı| W2["W2 100"] W2 -->|Başarılı| PROD["PROD ~523"] LAB -->|Başarısız| RB["Rollback +
kök neden"] P1 -->|Başarısız| RB P2 -->|Başarısız| RB W1 -->|Başarısız| RB W2 -->|Başarısız| RB ```
# 11 — Güvenlik Sertleştirme (Security Hardening) > Kısa özet: saha cihazının güvenlik tabanı: UFW default DENY, WireGuard arayüz izinleri, Docker-UFW bypass çözümü, SSH sertleştirme. Teknik ekip ve denetim içindir. > > - Dosya: `docs/11-SECURITY-HARDENING.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-05`, `#K-10`, `#K-13` > - Durum: [ ] Taslak --- ## 1. Amaç 700 saha cihazında varsayılan-kapalı ağ + en az ayrıcalık ilkesiyle saldırı yüzeyini küçültmek; Docker'ın UFW'yi baypas etme davranışını standart çözüme bağlamak. ## 2. Kapsam - Kapsam içi: UFW politikası, WireGuard arayüz kuralları, Docker bypass çözümü, SSH/userspace sertleştirme, denetim komutları. - Kapsam dışı: WireGuard kurulum detayı (08), Docker işletimi (12), kullanıcı/anahtar dağıtımı (06). ## 3. Kararlar ```text KARAR: UFW varsayılan politika: incoming DENY, outgoing ALLOW; izinler tek tek açılır. GEREKÇE: Varsayılan-kapalı, unutulan portu dışa kapatır; 700 cihazda en güvenli varsayılan budur. ALTERNATİF: Varsayılan ALLOW + kara liste — unutulan servis dışa açılır, elendi. RİSK: Kural unutulursa meşru trafik kesilir; azaltma: LAB'da kural seti test edilir, bf-status denetler. MALİYET: Ücretsiz. LİSANS: UFW (Ubuntu paketi, GPL bileşenler) — https://manpages.ubuntu.com/manpages/resolute/en/man8/ufw.8.html (doğrulanma: 2026-09-15). ``` ```text KARAR: SSH (22) ve RDP (3389) yalnızca WireGuard arayüzünden (`wg0`) kabul edilir; herkese açık internete kapalıdır. GEREKÇE: K-05 gereği yönetim düzlemi tünel arkasındadır; dış taramada port görünmez. ALTERNATİF: İnternete açık SSH + fail2ban — kaba-kuvvet yüzeyini gereksiz büyütür, elendi. RİSK: WireGuard çökerse SSH erişimi kesilir; azaltma: RustDesk/MeshCentral bağımsız kanallar (K-04). MALİYET: Ücretsiz. LİSANS: Yok (yapılandırma). ``` ```text KARAR: Docker-UFW bypass sorununun çözümü: yayımlanan tüm saha portları `127.0.0.1`'e bağlanır; `iptables:false` varsayılan çözüm değildir. GEREKÇE: Docker, yayımlanan portlarda UFW kurallarını baypas eder (resmi belgeli); localhost-bind, servisi dış arayüzden görünmez kılar, erişim WireGuard üzerinden olur. ALTERNATİF: `iptables:false` — resmi doküman "çoğu kullanıcı için uygun değil" diye uyarır; yedek seçenek olarak tutulur. RİSK: Yanlış bind ile port dışa açılır; azaltma: `docker ps` + `ss -tlnp` denetimi imaj final-check'te. MALİYET: Ücretsiz (Docker CE). LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/network/packet-filtering-firewalls/ (doğrulanma: 2026-09-15). ``` ```text KARAR: SSH sertleştirme: `blueforce` bakım kullanıcısı + anahtar zorunlu, password auth kapalı, root SSH kapalı (K-13 uygulanır). GEREKÇE: Kaba-kuvvet ve tek-key blast-radius riskini kapatır. ALTERNATİF: Password auth — saldırı yüzeyi nedeniyle elendi. RİSK: Anahtar kaybı erişimsizlik; azaltma: per-admin anahtar + Ansible ile merkezi dağıtım. MALİYET: Ücretsiz. LİSANS: OpenSSH (BSD-lisanslı), ücretsiz. ``` ## 4. Neden Bu Karar? Üç filtre: (1) varsayılan-kapalı (UFW DENY + WG-arkası yönetim), (2) Docker gerçeğine uyum (bypass inkâr edilmez, localhost-bind ile etkisizleştirilir), (3) ücretsiz katman (ek güvenlik ürünü yok). ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Varsayılan ALLOW + kara liste | Kolay | Unutulan port dışa açılır | Elendi | | İnternete açık SSH + fail2ban | Basit | Kaba-kuvvet yüzeyi | Elendi | | `iptables:false` | Bypass'ı kökten keser | Docker ağ işlevlerini bozar, resmi uyarı var | Yedek | | Ücretli EDR/host güvenlik ajanı | Merkezi konsol | Lisans maliyeti, sadelik ihlali | Elendi | ## 6. Avantajlar - Tek standart kural seti 700 cihaza aynen uygulanır; denetimi Ansible ile otomatik. - Docker bypass'ı mimari düzeyde çözülür, kural-dışı port açığı kalmaz. ## 7. Dezavantajlar - localhost-bind, konteyner portunu doğrudan LAN'dan kullanılamaz kılar (erişim WG üzerinden olur — bilinçli tercih). - UFW kural değişikliği hatalı playbook ile filoya yayılabilir; önce LAB(2). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Bypass yüzünden MEG portu dışa açılır | Orta | Yüksek | localhost-bind + final-check denetimi | | Kural hatası WG trafiğini keser | Düşük | Yüksek | LAB testi + `ufw --dry-run` + konsol erişimi | | Semaphore Community'de 2FA yok | Orta | Orta | Semaphore yalnızca WG arkasında | ## 9. Uygulama Planı 1. UFW varsayılanları ve taban kuralları uygulanır. 2. WireGuard arayüzüne SSH/RDP izni, dış dünyaya ret yazılır. 3. Docker portları `127.0.0.1` bind ile doğrulanır. 4. SSH sertleştirme (`PasswordAuthentication no`, `PermitRootLogin no`) uygulanır. ```bash # UFW taban politikası ufw default deny incoming ufw default allow outgoing ufw allow in on wg0 to any port 22 proto tcp ufw allow in on wg0 to any port 3389 proto tcp ufw allow 51820/udp ufw enable # durum denetimi ufw status verbose ss -tlnp | grep -v '127\.0\.0\.1' ``` ```bash # SSH sertleştirme denetimi sshd -T | grep -Ei 'passwordauthentication|permitrootlogin' ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `ufw status verbose` | default DENY incoming | LAB(2) | | Dış taramada 22/3389 | Kapalı (filtreli) | P1(5) | | `ss -tlnp` bind denetimi | `0.0.0.0` bind yok | LAB(2) | | Password ile SSH denemesi | Red | LAB(2) | | WG down iken dış SSH | Bağlantı yok | P1(5) | ## 11. Rollback 1. Hatalı kuralda önceki UFW profili geri yüklenir (`ufw reset` + bilinen-iyi kural seti Ansible'dan). 2. SSH kilitlenmesinde konsol/RustDesk üzerinden düzeltilir. 3. Son çare: golden image yeniden kurulumu (17-RECOVERY). ## 12. Kontrol Listesi - [ ] `ufw default deny incoming` tüm sahada aktif. - [ ] 22/3389 yalnızca `wg0` üzerinden erişilebilir. - [ ] Docker `0.0.0.0` bind yok. - [ ] `PasswordAuthentication no`, `PermitRootLogin no` doğrulandı. ## 13. Açık Sorular - [ ] WireGuard port numarası standardı (51820 varsayılan mı?) — 08 yazarı + LAB (sahibi: Faz 3). - [ ] Ek UFW log seviyesi/rate-limit değerleri — PİLOT ölçümü (sahibi: 11 yazarı). --- ## Ek: Mermaid — Güvenlik katmanları ```mermaid flowchart LR INT["Internet"] --> UFW["UFW
default DENY in"] UFW -->|51820/udp| WG["WireGuard wg0"] WG -->|22, 3389| MGMT["SSH/RDP"] UFW -->|RustDesk/MeshCentral
giden| RD["Bağımsız kanallar"] DKR["Docker
127.0.0.1 bind"] -.->|dışa kapalı| UFW ```
# 12 — Docker Operasyonları > Kısa özet: saha Docker disiplini: otostart, `unless-stopped`, pinli imaj, `latest` yasağı, log rotasyonu, Watchtower yok. Konteyner işleten teknik ekip içindir. > > - Dosya: `docs/12-DOCKER-OPERATIONS.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-10` > - Durum: [ ] Taslak --- ## 1. Amaç Elektrik kesintisi sonrası konteynerlerin kendiliğinden ve doğru sürümle ayağa kalkması; disiplinsiz imaj/log davranışının 700 cihazda filo arızasına dönüşmesini engellemek. ## 2. Kapsam - Kapsam içi: restart politikası, imaj pinleme, `latest`/Watchtower yasakları, log rotasyonu, port bind kuralı, otostart denetimi. - Kapsam dışı: MEG kabul testleri (13), log içerik politikası (15), boot zinciri systemd sırası (16). ## 3. Kararlar ```text KARAR: Tüm saha konteynerlerinde restart politikası `unless-stopped` kullanılır; Docker daemon açılışta otomatik başlar. GEREKÇE: `unless-stopped`, teknisyenin bakım için durdurduğunu reboot sonrası sürpriz çalıştırmaz; elektrik kesintisi "manuel stop" sayılmadığı için kurtarma aynen çalışır. Daemon enable olmadan otostart anlamsızdır. ALTERNATİF: `always` — bakım-stop'u reboot'ta deler, elendi. `on-failure`/`no` — kesinti kurtarması sağlamaz, elendi. RİSK: Yanlış politikayla kritik konteyner kalkmaz; azaltma: `docker inspect` final-check'te. MALİYET: Ücretsiz (Docker CE). LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/containers/start-containers-automatically/ (doğrulanma: 2026-09-15). ``` ```text KARAR: `latest` etiketi yasaktır; her imaj sabit tag ile pinlenir (örn. `meg:1.2.3`); digest pinleme (`@sha256`) PİLOT-2'de değerlendirilir. GEREKÇE: Hareketli `latest` 700 cihazda determinizmi bozar; "onaysız update yasak" ilkesi sabit tag gerektirir. ALTERNATİF: `latest` + periyodik pull — onaysız değişim riski, elendi. RİSK: Tag yeniden yazılırsa (mutable tag) pinleme delinir; azaltma: PİLOT-2'de digest kararı. MALİYET: Ücretsiz. LİSANS: Yok (operasyon politikası). ``` ```text KARAR: Watchtower (veya eşdeğeri otomatik güncelleyici) sahada yasaktır ve kurulmaz. GEREKÇE: Hem "onaysız update yasak" ilkesini ihlal eder hem upstream'de arşivlenmiştir. ALTERNATİF: Programlı otomatik pull — politika ihlali, elendi. RİSK: Manuel update gecikmesi; azaltma: 10-UPDATE dalga zinciri. MALİYET: Ücretsiz. LİSANS: Watchtower arşiv durumu — https://github.com/containrrr/watchtower (doğrulanma: 2026-09-15). ``` ```text KARAR: Log rotasyonu `daemon.json`'da zorunludur (`json-file`: `max-size` + `max-file`); rotasyonsuz log ile cihaz sahaya çıkmaz. GEREKÇE: Docker varsayılan `json-file` logu rotasyon yapmaz; dolan disk = kilitlenen cihaz (700 katı etki). ALTERNATİF: Rotasyonsuz + periyodik manuel temizlik — unutulur, elendi. RİSK: Değerler MEG hacmine göre yanlışsa log kaybolur veya disk dolar; azaltma: PİLOT'ta hacim ölçülür, değer sabitlenir. MALİYET: Ücretsiz. LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/logging/configure/ (doğrulanma: 2026-09-15). ``` ```text KARAR: Yayımlanan portlar `127.0.0.1`'e bağlanır (UFW bypass'a karşı); kural 11-HARDENING ile birlikte denetlenir. GEREKÇE: Docker yayımlanan portlarda UFW'yi baypas eder (resmi belgeli); localhost-bind dışa açılmayı kapatır. ALTERNATİF: `iptables:false` — resmi uyarı nedeniyle varsayılan değil, yedek. RİSK: Hatalı compose ile `0.0.0.0` bind; azaltma: final-check'te `ss -tlnp` denetimi. MALİYET: Ücretsiz. LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/network/packet-filtering-firewalls/ (doğrulanma: 2026-09-15). ``` ## 4. Neden Bu Karar? Tek ilke — öngörülebilir filo: cihaz neyle kapandıysa onunla açılır (unless-stopped), hangi sürüm onaylandıysa o çalışır (pinli tag), disk logla dolmaz (rotasyon), hiçbir şey kendiliğinden güncellenmez (Watchtower yok). ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | `always` | Her koşulda ayakta | Bakım davranışını bozar | Elendi | | `latest` serbest | Kolay pull | Determinizm yok | Elendi | | Watchtower | Otomatik güncellik | Politika ihlali + arşivli | Elendi | | `iptables:false` (varsayılan) | Bypass'ı keser | Resmi "çoğu kullanıcı için uygun değil" uyarısı | Yedek | ## 6. Avantajlar - Kesinti kurtarma ile bakım öngörülebilirliği aynı politikada birleşir. - Deterministik sürüm + rotasyonlu log = uzaktan tanı güvenilirliği. ## 7. Dezavantajlar - Sabit tag disiplini update prosedürüne yük bindirir (10-UPDATE ile işletilir). - Rotasyon değerleri PİLOT ölçümüne kadar kesinleşmez. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | `max-size/max-file` yanlış ayar | Orta | Orta | PİLOT hacim ölçümü | | Mutable tag yeniden yazımı | Düşük | Yüksek | Digest pinleme değerlendirmesi (PİLOT-2) | | Daemon enable unutulur | Düşük | Yüksek | install.sh final-check | ## 9. Uygulama Planı 1. `daemon.json` yazılır (log rotasyonu), daemon enable edilir. 2. Compose dosyalarında restart + sabit tag + localhost bind uygulanır. 3. Final-check ile `docker inspect` / `ss` denetimi yapılır. ```bash # daemon.json (örnek — boyut değerleri PİLOT ölçümüyle kesinleşir) cat /etc/docker/daemon.json # {"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}} systemctl enable --now docker # compose parçası (örnek) # ports: ["127.0.0.1:8080:8080"] # restart: unless-stopped # image: meg:1.2.3 # latest YASAK ``` ```bash # denetim docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.Config.Image}}' $(docker ps -aq) docker info --format '{{json .LoggingDriver}}' ss -tlnp | grep -v '127\.0\.0\.1' ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `docker inspect` restart | `unless-stopped` tamamında | LAB(2) | | İmaj tag denetimi | `latest` yok, sabit tag var | LAB(2) | | Güç kesintisi simülasyonu | Konteynerler otomatik kalkar | LAB(2) | | Manuel stop + reboot | Durdurulan kalkmaz | LAB(2) | | Log boyutu denetimi | `max-size/max-file` aşımı yok | P1(5) | ## 11. Rollback 1. Yanlış imajda önceki pinli tag'e dönüş (`docker compose pull/pull + up -d` onaylı sürümle). 2. Bozuk daemon.json'da bilinen-iyi dosya + `systemctl restart docker`. 3. Son çare: 10-UPDATE rollback zinciri. ## 12. Kontrol Listesi - [ ] `systemctl is-enabled docker` → enabled. - [ ] Tüm saha konteynerleri `unless-stopped`. - [ ] `latest` yok, sabit tag var. - [ ] Watchtower kurulu değil. - [ ] `daemon.json` rotasyon aktif. - [ ] `0.0.0.0` bind yok. ## 13. Açık Sorular - [ ] `max-size/max-file` kesin değerleri — PİLOT hacim ölçümü (sahibi: 12 yazarı). - [ ] Digest pinleme (`@sha256`) kararı — PİLOT-2 (sahibi: 12 yazarı). --- ## Ek: Mermaid — Konteyner yaşam döngüsü ```mermaid flowchart TB BOOT["Boot: docker enable"] --> UP["unless-stopped
otomatik kalkış"] UP --> PIN["Pinli tag
(latest yok)"] PIN --> LOG["Rotasyonlu log
daemon.json"] LOG --> BIND["127.0.0.1 bind
(UFW bypass kapalı)"] STOP["Manuel stop
(bakım)"] -.->|reboot sonrası kalkmaz| UP ```
# 13 — MEG Linux Kabul Kriterleri (Acceptance) > Kısa özet: MEG'in saha Linux'unda çalışır sayılması için 16 maddelik kabul listesi; paket içyapısı varsayılmaz, yalnızca gözlenebilir davranış test edilir. Kabul ekibi ve LAB sorumlusu içindir. > > - Dosya: `docs/13-MEG-LINUX-ACCEPTANCE.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-02`, `#K-10`, `#K-14` > - Durum: [ ] Taslak --- ## 1. Amaç "Çalışıyor" iddiasını denetlenebilir eşiğe bağlamak: MEG sürümü, GNOME + Docker zemininde 16 maddeyi geçmeden PİLOT dalgasına alınmaz. ## 2. Kapsam - Kapsam içi: kurulum/çalışma/sağlık/log/kurtarma gözlemleri (16 madde). - Kapsam dışı: MEG paket içyapısı, kaynak kodu, lisans koşulları (varsayılmaz; satıcı dokümanına havale). ## 3. Kararlar ```text KARAR: MEG kabulü 16 maddelik davranış listesidir; paket detayı varsayılmaz, her madde PASS/FAIL ile kapatılır. GEREKÇE: İçyapı varsayımı her satıcı sürümünde kırılır; gözlenebilir davranış (kurulur, kalkar, yanıt verir, log üretir, kurtarır) sürümler arası sabittir. ALTERNATİF: Paket-içi dosya yolu kontrolü — satıcı değişikliğinde test çöp olur, elendi. RİSK: Davranış testleri içsel bozulmayı geç fark eder; azaltma: metrik + log izleme (14/15) tamamlayıcıdır. MALİYET: Ücretsiz (test emeği). LİSANS: Yok (kabul prosedürü). ``` ```text KARAR: Kabul GNOME + `unless-stopped` + pinli tag zemininde koşar; `latest` ile kabul geçersizdir. GEREKÇE: K-02/K-10 zemini dışında geçen test saha gerçeğini temsil etmez. ALTERNATİF: XFCE/latest zeminde kabul — saha dışı, elendi. RİSK: Yok (zemin standardı 03/12'de kilitli). MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? MEG satıcı bağımlılığının en kırılgan noktasıdır; kabul listesi satıcının ne dağıttığına değil cihazın ne gösterdiğine bakar. Böylece sürüm değişse bile liste yaşar. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Paket-içi yol kontrolü | Hızlı | Sürümde kırılır | Elendi | | Tek "açılıyor mu" testi | Çok kolay | Kurtarma/log körlüğü | Elendi | | Satıcı beyanıyla kabul | Emeksiz | Doğrulanmamış | Elendi | ## 6. Avantajlar - 16 madde tek sayfada denetlenir; LAB(2) sonucu PİLOT kapısı olur. - Satıcı sürümü değişse liste değişmez. ## 7. Dezavantajlar - Liste, MEG işlevsel doğruluğunu (iş mantığı) test etmez; yalnızca platform uyumunu test eder. - 16 maddenin tamamı her sürümde yeniden koşulmalıdır. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | GNOME grafik bağımlılığı/ek ayar eksik | Orta | Yüksek | Madde 9–10 erken yakalar | | Log hacmi diski doldurur | Orta | Orta | Madde 12 + 12-DOCKER rotasyonu | | Kesinti sonrası MEG kalkmaz | Düşük | Yüksek | Madde 13–14 | ## 9. Uygulama Planı Kabul, LAB(2) cihazında pinli tag ile koşar; her madde PASS/FAIL işlenir, sonuç kabul tutanağına yazılır. ```bash # kabul öncesi zemin denetimi docker inspect --format '{{.HostConfig.RestartPolicy.Name}} {{.Config.Image}}' systemctl is-enabled docker ufw status verbose ``` ### MEG ACCEPTANCE — 16 madde | # | Madde | Nasıl bakılır | PASS eşiği | |---|---|---|---| | 1 | Kurulum tamamlanır | Onaylı prosedürle kurulum uca kadar gider | Hatasız tamamlanır | | 2 | Sabit sürüm kimliği | Sürüm/etiket sorgulanır | `latest` yok, sürüm okunur ve tutanakla eşleşir | | 3 | Konteyner/servis ayakta | `docker ps` / servis durumu | Çalışıyor (restart döngüsü yok) | | 4 | Restart politikası | `docker inspect` | `unless-stopped` | | 5 | Port bind | `ss -tlnp` | Dışa açık `0.0.0.0` bind yok | | 6 | Sağlık yanıtı | Sağlık endpoint'i/komutu (varsa) veya süreç canlılığı | Yanıt veriyor / süreç yaşıyor | | 7 | Temel işlev duman testi | Tanımlı en küçük iş akışı uçtan uca | Hatasız tamamlanır | | 8 | Yeniden başlatmada toparlanma | Konteyner restart edilir | ≤ tanımlı sürede sağlığa döner | | 9 | GNOME uyumu | GNOME oturumunda grafik gereksinim (varsa) açılır | Görüntü/oturum hatasız | | 10 | RDP üzerinden erişim | xRDP + GNOME ile uzaktan oturum | Oturum açılır, kritik hata yok | | 11 | Log üretimi | Log çıktısı gözlenir | Tanımlı olaylar logda görülür | | 12 | Log rotasyonu altında çalışma | Rotasyon aktifken 24 saat çalışma | Disk taşması yok, log kaybı kritik değil | | 13 | Güç kesintisi kurtarması | Güç kesintisi simülasyonu | Boot sonrası kendiliğinden sağlığa döner | | 14 | WireGuard yokluğunda çalışma | WG down edilir | Yerel işlev sürer (bağımlılık varsa tutanağa yazılır) | | 15 | Monitoring görünürlüğü | Prometheus/Grafana + Kuma | Cihaz ve MEG sağlığı panoda görünür | | 16 | Destek paketi üretimi | `bf-support-bundle` çalıştırılır | Secret sızdırmadan paket üretilir | ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | 16 maddenin tamamı LAB(2)'de | Tümü PASS | LAB(2) | | Başarısız maddede sürüm reddi | PİLOT'a alınmaz, satıcıya iade | LAB(2) | | PASS sürümün P1(5) tekrarı | 1,3,6,8,13,15 yeniden PASS | P1(5) | ## 11. Rollback 1. Kabulü geçemeyen sürüm sahaya sürülmez; önceki kabul görmüş pinli tag sahada kalır. 2. Yanlışlıkla sürülen sürüm 10-UPDATE rollback zinciriyle geri alınır. ## 12. Kontrol Listesi - [ ] 16 maddenin PASS/FAIL kaydı tutanakta var. - [ ] Sürüm kimliği tutanakla eşleşiyor. - [ ] `latest` kullanılmadı. - [ ] Başarısız madde varsa PİLOT onayı verilmedi. ## 13. Açık Sorular - [ ] Madde 8/13 için süre eşiği (örn. ≤5 dk) — PİLOT ölçümüyle sabitlenecek (sahibi: 13 yazarı). - [ ] Madde 7 duman testinin kapsamı — MEG işlev sahibiyle tanımlanacak (sahibi: Faz 3). --- ## Ek: Mermaid — Kabul kapısı ```mermaid flowchart LR VER["MEG sürümü"] --> LAB["LAB 2: 16 madde"] LAB -->|Tümü PASS| P1["PILOT-1 5: kritik 6 madde"] LAB -->|Herhangi FAIL| REJ["RED: sahaya alınmaz"] P1 -->|PASS| WAVE["Dalgalar (10-UPDATE)"] P1 -->|FAIL| REJ ``` # 14 — İzleme ve Sağlık (Monitoring & Health) > Kısa özet: 17 metrik + Prometheus/Node Exporter/Grafana OSS + Uptime Kuma + `bf-status`/`bf-diagnostics`/`bf-support-bundle` sözleşmeleri. Monitoring operatörü ve saha teknisyeni içindir. > > - Dosya: `docs/14-MONITORING-AND-HEALTH.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-07`, `#K-14` > - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazda "hangisi ölü, hangisi hasta, hangisi sağlıklı" sorusuna iki perdede cevap: mühendis için metrik (Prometheus), teknisyen için UP/DOWN + last-seen (Uptime Kuma). ## 2. Kapsam - Kapsam içi: 17 metrik listesi, Prometheus/Node Exporter/Grafana OSS/Uptime Kuma rolleri, scrape/retention ilkeleri, 3 saha komutu sözleşmesi. - Kapsam dışı: log içerik politikası (15), alarm bildirim hedefleri detayı (kurulum dokümanına havale), merkez donanım siparişi. ## 3. Kararlar ```text KARAR: Birincil metrik yığını Prometheus + Node Exporter + Grafana OSS'tir; 17 metriğin tamamı burada toplanır. GEREKÇE: 17 metriğin tamamı yalnızca bu yığında ücretsiz ve kotasız toplanır (WireGuard/Docker/MEG sağlığı textfile collector ile). ALTERNATİF: Netdata Cloud — Free 5 node kotası 700 cihazda ücrete düşer, elendi. Yalnızca Kuma — metrik körlüğü, elendi. RİSK: 700 node kardinalite/saklama yükü; azaltma: 60 sn scrape, 30–90 gün retention, `BF-` etiket şeması. MALİYET: Ücretsiz. LİSANS: Prometheus/Node Exporter Apache-2.0 — https://github.com/prometheus/prometheus + https://prometheus.io/docs/guides/node-exporter/ (2026-09-15); Grafana OSS AGPL-3.0 — https://grafana.com/oss/ (2026-09-15). ``` ```text KARAR: Tamamlayıcı olarak Uptime Kuma: saha-teknisyeni dostu UP/DOWN panosu + Push "last seen" + bildirimler. GEREKÇE: Metrik bilmeyen teknisyene en basit çevrimdışı alarmını verir (20 sn aralık, 90+ bildirim servisi, tek konteyner). ALTERNATİF: Yalnızca Grafana alarmı — teknisyen için karmaşık, elendi. RİSK: Kuma metrik vermez; azaltma: Prometheus ile birlikte koşar, rol çakışması yok. MALİYET: Ücretsiz. LİSANS: Uptime Kuma MIT — https://github.com/louislam/uptime-kuma (2026-09-15). ``` ```text KARAR: Özel metrikler (WireGuard handshake, Docker, MEG) Node Exporter textfile collector `.prom` dosyalarıyla verilir. GEREKÇE: Standart Prometheus yolu; özel exporter yazma yükü yok. ALTERNATİF: Özel exporter geliştirme — bakım yükü, elendi. RİSK: `.prom` yazım hatası scrape'i bozar; azaltma: cron/systemd timer çıktısı atomik yazılır. MALİYET: Ücretsiz. LİSANS: Node Exporter Apache-2.0 — https://github.com/prometheus/node_exporter (2026-09-15). ``` ```text KARAR: Saha sağlık sözleşmeleri: `bf-status` (hızlı bakış), `bf-diagnostics` (derin tanı), `bf-support-bundle` (secret sızdırmaz paket). GEREKÇE: 700 dokunulmaz cihazda logsuz metrik kör, metriksiz log sağırdır; bundle secret dışlama kuralıyla güvenli uzaktan tanı sağlar. ALTERNATİF: Harici log platformu (Loki/ELK) ilk fazda — ek servis yükü, elendi (Faz-sonrası hedef). RİSK: Bundle'a secret kaçması; azaltma: dışlama listesi + üreticide maskeleme testi. MALİYET: Ücretsiz. LİSANS: systemd-journald / textfile (Apache-2.0) — https://github.com/prometheus/node_exporter (2026-09-15). ``` ## 4. Neden Bu Karar? İki kullanıcı, iki hız: mühendis kök nedeni metrikten bulur, teknisyen kesintiyi Kuma'dan görür. Scrape 60 sn + retention 30–90 gün, 700 node'da merkez yükünü taşınabilir tutar. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Yalnızca Uptime Kuma | Çok kolay | Metrik körlüğü | Elendi | | Netdata Cloud | Zengin agent | Free 5 node kotası | Elendi | | Netdata Parent self-hosted | Zengin | Merkezi tasarım yükü + kota riski | Elendi | | Loki/ELK ilk fazda | Güçlü log | Ek servis yükü, sadelik ihlali | Faz-sonrasına ertelendi | ## 6. Avantajlar - Tamamı ücretsiz ve kotasız; 700 cihaz lisans duvarına çarpmaz. - Tek kimlik (`BF-`) metrikten loga join anahtarıdır. ## 7. Dezavantajlar - Prometheus 700 node işletme bilgisi ister; retention LAB ölçümü olmadan donanım siparişi verilmemeli. - Kuma + Prometheus çift kurulum merkezde iki servis demektir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Kardinalite patlaması | Orta | Orta | 60 sn scrape, etiket disiplini, federasyon hedefi | | Retention şişmesi | Orta | Orta | 30–90 gün, PİLOT hacim ölçümü | | Bundle secret sızıntısı | Düşük | Yüksek | Dışlama listesi + maskeleme testi | ## 9. Uygulama Planı 1. Merkez: Prometheus + Grafana OSS + Uptime Kuma kurulur. 2. Saha: Node Exporter + textfile cron/timer + Kuma Push kurulur. 3. Etiket şeması `device_id="BF-"` tüm metriklerde uygulanır. 4. `bf-status` / `bf-diagnostics` / `bf-support-bundle` sahaya dağıtılır. ```bash # saha: Node Exporter + textfile dizini (örnek) systemctl enable --now node_exporter ls /var/lib/node_exporter/textfile/ # *.prom buraya atomik yazılır # WireGuard handshake örneği (üretici script özeti) # wg show wg0 latest-handshakes > /var/lib/node_exporter/textfile/wireguard.prom.tmp \ # && mv .../wireguard.prom.tmp .../wireguard.prom ``` ### 17 metrik listesi | # | Metrik | Kaynak | Ne için | |---|---|---|---| | 1 | `up` | Prometheus scrape | Cihaz scrape edilebilir mi (ölü/diri) | | 2 | `node_cpu_seconds_total` | Node Exporter | CPU yükü | | 3 | `node_memory_MemAvailable_bytes` | Node Exporter | RAM baskısı | | 4 | `node_filesystem_avail_bytes` | Node Exporter | Disk dolumu | | 5 | `node_load1/5/15` | Node Exporter | Yük ortalaması | | 6 | `node_network_receive/transmit_bytes_total` | Node Exporter | Ağ trafiği | | 7 | `node_systemd_unit_state` | Node Exporter (systemd) | Kritik servis ayakta mı | | 8 | `node_timex_offset_seconds` | Node Exporter | Saat kayması (TLS/WG için kritik) | | 9 | `node_boot_time_seconds` | Node Exporter | Beklenmedik reboot tespiti | | 10 | `wireguard_last_handshake_seconds` | textfile | Tünel canlılığı (stale = kopuk) | | 11 | `wireguard_transfer_bytes` | textfile | Tünel trafiği | | 12 | `docker_container_running` | textfile | MEG/yan konteyner ayakta mı | | 13 | `docker_container_restart_count` | textfile | Crash-loop tespiti | | 14 | `meg_up` | textfile | MEG sağlığı (kabul madde 6 ile aynı kaynaktan) | | 15 | `meg_last_success_timestamp` | textfile | MEG iş yapıyor mu (stale = takılı) | | 16 | `unattended_upgrade_active` | textfile | K-11 kilidi delinmiş mi (1 = ihlal) | | 17 | `kuma_push_last_seen` | Uptime Kuma Push | Teknisyen "last seen" görünümü | Scrape: 60 sn. Retention: 30–90 gün (LAB ölçümüyle kesinleşir). ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Scrape + 17 metrik | Grafana panosu dolu | P1(5) | | WG down simülasyonu | Metrik 10 stale + Kuma DOWN | P1(5) | | Konteyner kill | Metrik 12/13 alarm | LAB(2) | | `unattended-upgrade` açma | Metrik 16 = 1 alarm | LAB(2) | | Kuma Push kesintisi | last-seen bayatlar | P1(5) | ## 11. Rollback 1. Bozuk exporter/push, önceki sürüme Ansible ile geri alınır. 2. Prometheus config hatasında önceki config + `promtool check config` ile dönüş. 3. Son çare: monitoring'siz işletim (erişim kanalları bağımsızdır — 01). ## 12. Kontrol Listesi - [ ] 17 metriğin tamamı Grafana'da görünüyor. - [ ] Kuma UP/DOWN + Push last-seen çalışıyor. - [ ] Etiket şeması `BF-` tutarlı. - [ ] Scrape 60 sn, retention 30–90 gün aralığında. ### `bf-status` örnek çıktısı (16 satır sözleşmesi) ```text BF-12010193 2026-09-15 12:00 +03 OK uptime: 6d 4h 12m | load: 0.31 0.28 0.22 cpu: 8% | mem: 1.1/3.8G (29%) | disk /: 9.2/28G (33%) net: wg0 UP, handshake 12s ago | rx 41M tx 18M docker: daemon UP | meg:1.2.3 running 6d (restarts 0) meg: UP, last success 40s ago systemd: docker OK | wg-quick@wg0 OK | xrdp OFF | node_exporter OK unattended-upgrades: DISABLED (timer masked) ufw: active, default DENY in | 22/3389 wg0-only gui: OFF (multi-user) | rdp sessions: 0 kuma push: OK 30s ago | prometheus scrape: OK 45s ago rustdesk: online | meshcentral: online boot: 2026-09-09 07:48 (power-loss recovery OK) alerts: 0 critical, 1 warning (disk trend) next: support bundle: bf-support-bundle ``` `bf-diagnostics`: yukarıdakinin detaylı sürümü (servis log kuyrukları + `ss` + `df -i` + textfile içerikleri). `bf-support-bundle`: aynı paketin tarball'ı; WireGuard private key, RustDesk şifresi, SSH private key ve `authorized_keys` dışı secret'lar ASLA pakete girmez (üretici maskeleme testinden geçer). ## 13. Açık Sorular - [ ] Prometheus retention/kardinalite hesabı (700 × 60 sn) — LAB ölçümü (sahibi: 14 yazarı). - [ ] Bildirim hedefleri (SMS/e-posta/webhook) — merkez kurulumunda netleşecek (sahibi: Faz 3). - [ ] Federasyon gerekip gerekmediği — 700 node ölçümü (sahibi: 14 yazarı). --- ## Ek: Mermaid — Metrik akışı ```mermaid flowchart LR SAHA["Saha bf-
Node Exporter + textfile"] -->|scrape 60sn| PROM["Prometheus"] PROM --> GRAF["Grafana OSS"] SAHA -->|Push last-seen| KUMA["Uptime Kuma"] KUMA --> TECH["Teknisyen panosu"] GRAF --> ENG["Mühendis panosu"] SAHA -->|bf-status| CONSOLE["Saha konsolu"] ```
# 15 — Loglama (Logging) > Kısa özet: 7 log kaynağı (journald/Docker/RustDesk/WG/MEG/Blueforce/installer/update-history) için yer, rotasyon ve saklama limitleri. Tanı koyan mühendis ve denetçi içindir. > > - Dosya: `docs/15-LOGGING.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-10`, `#K-14` > - Durum: [ ] Taslak --- ## 1. Amaç Disk doldurmayan ama tanı koydurmaya yeten log disiplini: her kaynağın nerede tutulduğu, ne kadar büyüyebileceği ve ne zaman silineceği bellidir. ## 2. Kapsam - Kapsam içi: 7 kaynağın konumu + rotasyon + saklama limiti; secret maskeleme kuralı. - Kapsam dışı: metrik tanımları (14), merkezi log platformu (Faz-sonrası hedef), log içeriklerinin iş anlamı. ## 3. Kararlar ```text KARAR: journald kalıcı + boyut sınırlı tutulur (`SystemMaxUse`/`MaxFileSec` ile); sınırsız journal yasaktır. GEREKÇE: Sınırsız journal saha diskini sessizce doldurur; sınırlı journal boot sonrası tanıya yetecek kadar geçmişi korur. ALTERNATİF: Volatil journal (RAM'de) — reboot sonrası tanı kaybolur, elendi. RİSK: Limit küçükse kesinti kök nedeni silinir; azaltma: PİLOT hacim ölçümü. MALİYET: Ücretsiz. LİSANS: systemd-journald (LGPL bileşenler), ücretsiz. ``` ```text KARAR: Docker `json-file` rotasyonu (`max-size`/`max-file`) tüm saha konteynerlerinde zorunludur (K-10). GEREKÇE: Rotasyonsuz Docker logu disk doldurur = kilitlenen cihaz. ALTERNATİF: Rotasyonsuz + manuel temizlik — unutulur, elendi. RİSK: Değerler MEG hacmine göre ayarlanmazsa kayıp/taşma; azaltma: PİLOT ölçümü (12 ile ortak). MALİYET: Ücretsiz. LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/logging/configure/ (doğrulanma: 2026-09-15). ``` ```text KARAR: Harici log platformu (Loki/ELK) ilk fazda kurulmaz; Faz-sonrası hedef olarak izlenir. GEREKÇE: Ek servis yükü sadelik kuralını bozar; 7 kaynak + bundle ilk faz tanısı için yeterlidir. ALTERNATİF: İlk fazda merkezi log — merkez yükü + işletme maliyeti, ertelendi. RİSK: Filo-çapı log araması ilk fazda yoktur; azaltma: Ansible ile hedefli log toplama. MALİYET: Ücretsiz (erteleme). LİSANS: Yok. ``` ## 4. Neden Bu Karar? 700 cihaz × sınırsız log = 700 dolu disk. Her kaynağa tavan konur; tavanlar PİLOT'ta ölçülen gerçek hacimle sabitlenir. Merkezi platform yokluğunda tanı, cihaz-üzeri komutlar + hedefli toplama ile yapılır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Sınırsız journal | Kayıpsız geçmiş | Disk taşması | Elendi | | Volatil journal | Disk dostu | Reboot sonrası körlük | Elendi | | İlk fazda Loki/ELK | Merkezi arama | Servis yükü | Ertelendi | | Rotasyonsuz Docker log | Sıfır ayar | Kilitlenen cihaz | Elendi | ## 6. Avantajlar - Tanı için yeterli geçmiş, disk için güvenli tavan aynı tabloda. - 7 kaynak standardı `bf-support-bundle` ile birebir eşleşir. ## 7. Dezavantajlar - Merkezi arama yok; filo-çapı örüntü avı Ansible hedefli toplamayla sınırlı. - Limitler PİLOT ölçümüne kadar taslak değerlerdir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Limit küçük → kök neden silinir | Orta | Orta | PİLOT hacim ölçümü + kritik olayda bundle | | MEG log patlaması | Orta | Yüksek | Rotasyon + metrik 13 alarmı | | Secret loga yazılır | Düşük | Yüksek | Maskeleme kuralı + bundle dışlama listesi | ## 9. Uygulama Planı 1. journald limitleri yazılır, kalıcı depolama açılır. 2. `daemon.json` rotasyonu uygulanır (12 ile ortak). 3. Uygulama log dizinleri + logrotate kurulur. 4. Update-history append-only tutulur. ```bash # journald (taslak değerler — PİLOT ölçümüyle kesinleşir) # /etc/systemd/journald.conf.d/blueforce.conf # [Journal] # Storage=persistent # SystemMaxUse=500M # MaxFileSec=14day systemctl restart systemd-journald journalctl --disk-usage # Docker rotasyon denetimi (12 ile ortak) cat /etc/docker/daemon.json ``` ### 7 kaynak — yer ve limit tablosu | # | Kaynak | Yer | Rotasyon/limit (taslak) | |---|---|---|---| | 1 | journald (sistem + servisler) | `/var/log/journal/` | `SystemMaxUse=500M`, `MaxFileSec=14day` | | 2 | Docker konteyner logları | `json-file` (daemon.json) | `max-size=10m`, `max-file=3` (PİLOT ile kesinleşir) | | 3 | RustDesk istemci | `~/.config/RustDesk/logs/` + journald | 50M tavan, 14 gün (istemci varsayılanını aşmayacak şekilde) | | 4 | WireGuard | journald (`wg-quick@wg0`) + textfile sayaç | journald limitine tabi; handshake sayacı metrikte (14) | | 5 | MEG | Konteyner logu (json-file) + varsa `/var/log/meg/` | Docker rotasyonuna tabi; dosya logu varsa logrotate 7×10M | | 6 | Blueforce servisleri | journald + `/var/log/blueforce/` | logrotate 7×10M, 30 gün | | 7 | installer + update-history | `/var/log/blueforce/install.log`, `/var/log/blueforce/update-history.log` | Append-only, rotasyonsuz saklanır (denetim izi); 50M üstünde arşivlenir, silinmez | Secret kuralı: private key, parola, token loga ve bundle'a ASLA yazılmaz; tanı için gereken kimlik yalnızca `BF-` etiketidir. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `journalctl --disk-usage` | Tavan altında | LAB(2) | | 7 gün çalışma sonrası disk | Taşma yok | P1(5) | | Logrotate kuru çalıştırma | Hatasız döner | LAB(2) | | Bundle secret taraması | Secret yok | LAB(2) | | update-history append | Silinmemiş, sıralı | LAB(2) | ## 11. Rollback 1. Hatalı journald/logrotate config'de önceki dosya geri yüklenir, servis restart edilir. 2. Aşırı agresif rotasyonda limit genişletilir, PİLOT ölçümü yenilenir. ## 12. Kontrol Listesi - [ ] journald kalıcı + tavanlı. - [ ] Docker rotasyonu aktif (12 ile ortak denetim). - [ ] 7 kaynağın tamamı bundle'a dahil veya bilerek hariç. - [ ] Secret taraması temiz. - [ ] update-history append-only. ## 13. Açık Sorular - [ ] journald/Docker/MEG kesin limitleri — PİLOT hacim ölçümü (sahibi: 15 yazarı, 12 ile ortak). - [ ] Merkezi log platformu (Loki) Faz-sonrası eşiği — 700 node ölçümü (sahibi: 14 yazarı). --- ## Ek: Mermaid — Log akışı ```mermaid flowchart LR J["journald
sistem + servisler"] --> B["bf-support-bundle
(secret sızdırmaz)"] D["Docker json-file
rotasyonlu"] --> B A["/var/log/blueforce/
+ update-history"] --> B B --> T["Merkez tanı
(Ansible hedefli toplama)"] ```
# 16 — Elektrik Kesintisi ve Otomatik Kurtarma > Kısa özet: güç geldikten sonra cihazın dokunulmadan işe dönmesi: BIOS ayarı, boot zinciri, systemd bağımlılıkları, kurtarma doğrulaması. Saha ve LAB ekibi içindir. > > - Dosya: `docs/16-POWER-LOSS-AND-AUTO-RECOVERY.md` > - İlgili kararlar: `24-DECISION-LOG.md#K-03`, `#K-05`, `#K-10` > - Durum: [ ] Taslak --- ## 1. Amaç Elektrik kesintisini sıradan olay haline getirmek: güç gelir, cihaz açılır, VPN kurulur, konteynerler kalkar, MEG işe döner — teknisyen dokunmaz. ## 2. Kapsam - Kapsam içi: BIOS güç ayarı, boot hedefi, systemd sıralaması ve bağımlılıkları, kurtarma testi. - Kapsam dışı: UPS yatırımı, veri yedekleme (17), update rollback (10). ## 3. Kararlar ```text KARAR: BIOS "Restore on AC Power Loss = Power On" tüm sahada standarttır; cihaz terminale (multi-user) boot eder. GEREKÇE: Güç gelince açılmayan cihaz kurtarma zincirine hiç giremez; terminal boot en düşük kaynakla en öngörülebilir zemindir (K-03). ALTERNATİF: Grafik boot / manuel açma — kaynak israfı + saha ziyareti, elendi. RİSK: Heterojen BIOS menülerinde ayar adı farklı olabilir; azaltma: donanım profil başına LAB kaydı. MALİYET: Ücretsiz. LİSANS: Yok (donanım ayarı). ``` ```text KARAR: Boot zinciri systemd bağımlılıklarıyla sıralanır: network → `wg-quick@wg0` → `docker.service` → MEG/uygulama birimleri → remote/monitoring ajanları. GEREKÇE: Sırasız boot race üretir (Docker WG'den önce kalkar, MEG ağ bulamaz); `After=`/`Wants=` zinciri her açılışta aynı sırayı garanti eder. ALTERNATİF: Bağımlılıksız "hepsi enable" — açılışta rastgele sıra, elendi. RİSK: Birim adları 26.04.1'e göre değişebilir; azaltma: LAB imajında doğrulanmadan dondurulmaz (K-03 riski). MALİYET: Ücretsiz. LİSANS: Yok (systemd yapılandırması). ``` ```text KARAR: Kurtarma `unless-stopped` + `wg-quick enable` + daemon enable üzerine kurulur; kesinti "manuel stop" sayılmaz. GEREKÇE: K-10 gereği kesinti sonrası konteynerler kendiliğinden kalkar; K-05 gereği VPN kendiliğinden gelir. Ek kurtarma servisi gerekmez. ALTERNATİF: Özel kurtarma scripti/cron — hareketli parça artar, elendi. RİSK: Yanlışlıkla stop edilen kritik konteyner kalkmaz (tasarlanan davranış); azaltma: `bf-status` ile görünürlük. MALİYET: Ücretsiz. LİSANS: Docker CE ücretsiz — https://docs.docker.com/engine/containers/start-containers-automatically/ (doğrulanma: 2026-09-15). ``` ## 4. Neden Bu Karar? Kurtarma, ek yazılımla değil sıralamayla çözülür: her katman bir öncekinin hazır olduğuna bakarak kalkar. Sadelik kuralı burada en literally uygulanır — zincirde özel bileşen yok. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | UPS ile kesintiyi önleme | Kesinti yaşanmaz | 700 cihazda donanım maliyeti | Saha kararına bırakıldı (bu dokümanın dışı) | | Özel kurtarma servisi | Esnek | Ek bakım yükü | Elendi | | Grafik boot | Teknisyene tanıdık | Kaynak + hata yüzeyi | Elendi | ## 6. Avantajlar - Kurtarma testi tek komutla tekrarlanır (fişi çek / reboot); sonuç deterministiktir. - Zincirdeki her halka standart systemd mekanizmasıdır, özel bilgi gerektirmez. ## 7. Dezavantajlar - Uzun kesintide saat kayması olabilir; NTP senkronu zincire dahildir ama ilk WG handshake gecikebilir. - Bozuk diskte (kirli filesystem) boot fsck'e takılabilir; journal + ext4 varsayılanı bu riski küçültür. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Boot race (sıra bozuk) | Orta | Orta | `After=` zinciri + LAB güç testi | | BIOS ayarı profilde yok | Düşük | Yüksek | Profil başına LAB kaydı | | Saat kaymasıyla WG/TLS gecikmesi | Orta | Düşük | NTP bekleme + handshake metriği (14) | ## 9. Uygulama Planı 1. BIOS güç ayarı profillere işlenir. 2. Boot hedefi multi-user yapılır; GUI kapalı standarttır. 3. systemd bağımlılıkları yazılır, enable zinciri kurulur. 4. Güç kesintisi simülasyonuyla doğrulanır. ```bash # boot hedefi (terminal standart) systemctl set-default multi-user.target systemctl get-default # enable zinciri systemctl enable wg-quick@wg0 docker node_exporter # MEG/uygulama birimleri docker sonrası kalkar: # /etc/systemd/system/meg-app.service.d/override.conf # [Unit] # After=docker.service wg-quick@wg0.service network-online.target # Wants=network-online.target # [Install] # WantedBy=multi-user.target # zincir denetimi systemd-analyze critical-chain docker.service systemd-analyze critical-chain wg-quick@wg0.service ``` ```bash # kurtarma doğrulaması (güç testi sonrası) uptime -s systemctl is-active wg-quick@wg0 docker docker ps --format '{{.Names}} {{.Status}}' bf-status ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Fiş çekme simülasyonu (güç kesintisi) | Müdahalesiz tam zincir kalkar | LAB(2) | | Reboot sonrası sıra | network → WG → Docker → MEG | LAB(2) | | Manuel stop + reboot | Durdurulan kalkmaz (tasarlanan) | LAB(2) | | 10 ardışık güç döngüsü | 10/10 kurtarma | P1(5) | | WG handshake süresi | Metrik 10 makul aralıkta tazelenir | P1(5) | ## 11. Rollback 1. Bozuk bağımlılıkta override dosyası kaldırılır, daemon-reload + reboot ile önceki sıra geri gelir. 2. Boot hedefine takılmada konsoldan `systemctl set-default` düzeltilir. 3. Son çare: golden image yeniden kurulumu (17-RECOVERY LEVEL 9). ## 12. Kontrol Listesi - [ ] BIOS güç ayarı profilde kayıtlı. - [ ] Boot hedefi multi-user. - [ ] `wg-quick@wg0`, `docker`, ajanlar enable. - [ ] `After=` zinciri dosyada ve LAB'da doğrulandı. - [ ] Güç testi 10/10 geçti (P1). ## 13. Açık Sorular - [ ] Kesin birim adları + GNOME oturum satırı — LAB 26.04.1 imajı (sahibi: Faz 3, 03/04 yazarları). - [ ] NTP bekleme süresi eşiği — P1 ölçümü (sahibi: 16 yazarı). --- ## Ek: Mermaid — Boot zinciri ve systemd bağımlılıkları ```mermaid flowchart TB PWR["Power ON
(BIOS Power On)"] --> TGT["multi-user.target
(terminal boot)"] TGT --> NET["network-online.target"] NET --> WG["wg-quick@wg0
After network-online"] WG --> DKR["docker.service
After network"] DKR --> MEG["MEG / uygulama birimleri
After docker + wg-quick"] MEG --> AGT["RustDesk + MeshCentral
+ node_exporter + Kuma Push"] AGT --> BF["bf-status OK"] ```
# 17 — Yedekleme, Kurtarma ve Yeniden Kurulum (Backup, Recovery & Reinstall) > Kısa özet: 9 seviyeli kurtarma merdiveni (LEVEL 1…9): servis yeniden başlatmadan merkez VDS'in sıfırdan inşasına kadar her seviyede merkezi prosedür + saha/yerel prosedür. Saha teknisyeni ve merkezi admin için ortak başvuru noktasıdır. - Dosya: `docs/17-BACKUP-RECOVERY-AND-REINSTALL.md` - İlgili kararlar: `24-DECISION-LOG.md#K-04` (kanal bağımsızlığı), `#K-05` (WireGuard), `#K-06` (Ansible/Semaphore), `#K-09` (golden image), `#K-10` (unless-stopped), `#K-11` (onaysız update yasağı), `#K-12` (cihaz kimliği), `#K-14` (support bundle) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazlı filoda her arızanın boyuna uygun, önceden tanımlı bir kurtarma yolu olması. Küçük arıza küçük müdahaleyle (servis restart), büyük arıza büyük müdahaleyle (yeniden imaj) çözülür; teknisyen "ne yapsam?" diye doğaçlama yapmaz, LEVEL tablosuna bakar. ## 2. Kapsam - Kapsam içi: saha cihazı LEVEL 1…8 kurtarma, merkez VDS yedeği ve yeniden inşası (LEVEL 9), her seviyede merkezi (uzaktan) + yerel (sahada) prosedür, yedeklenecek veri listesi, yedek sıklığı. - Kapsam dışı: update dalgalarının onay akışı (10-UPDATE), MEG uygulama verisi yedeği detayı (13-MEG-ACCEPTANCE'a havale), elektrik kesintisi sonrası otomatik toparlanma zinciri (16-POWER-LOSS). ## 3. Kararlar ```text KARAR: Kurtarma 9 seviyeli merdivenle işletilir: L1 servis restart → L2 reboot → L3 son değişikliği geri alma → L4 WireGuard/RustDesk/MeshCentral kanalıyla uzaktan onarım → L5 yedekten config geri yükleme → L6 Docker/MEG yeniden kurulum → L7 OS onarımı → L8 USB ile sıfırdan imaj → L9 merkez VDS yeniden inşası. GEREKÇE: Merdiven, en ucuz müdahaleyi önce dener; gereksiz yeniden imaj (veri kaybı + teknisyen saati) ve gereksiz VDS müdahalesi önlenir. Her seviyenin "merkezi" ve "yerel" kolu vardır, erişim durumuna göre yol seçilir. ALTERNATİF: Tek "USB ile sıfırla" prosedürü — her arızada saha ziyareti gerektirir, 700 cihazda sürdürülemez; elendi. RİSK: Yanlış seviye seçimi (L8 gerekirken L1'de oyalanma); azaltma: her seviyede "bir üst seviyeye geç" kriteri yazılıdır. MALİYET: Ücretsiz (yerleşik araçlar + Ansible). LİSANS: Yok (işletim prosedürü; araç lisansları 24-DECISION-LOG Ek A). ``` ```text KARAR: Merkez VDS'ler dosya-seviyesi yedek + IaC (Ansible playbook + compose dosyaları Git'te) ile kurtarılır; blok-imaj yedeği zorunlu değildir. GEREKÇE: Servislerin tamamı kodla yeniden kurulabilir durumda tutulur (playbook/compose Git'te); veri (envanter DB, Prometheus verisi, Semaphore DB) dosya yedeğiyle alınır. Blok imaj hem pahalı hem VDS sağlayıcıya bağımlıdır. ALTERNATİF: Sağlayıcı snapshot'ına bel bağlama — taşınabilir değil, ikinci VDS'e geçemez; yedek yöntem olarak tutulur, birincil yapılmaz. RİSK: Git'teki IaC ile gerçek VDS drift eder; azaltma: üç ayda bir "boş VDS'e sıfırdan kur" tatbikatı (L9 testi). MALİYET: Ücretsiz (VDS kira + varsa snapshot ücreti hariç). LİSANS: Yok. ``` ```text KARAR: Saha cihazında kullanıcı verisi yedeği alınmaz; cihaz "değiştirilebilir" (disposable) sayılır — kimlik (`bf-`) + config merkezden yeniden basılır. GEREKÇE: Tek kimlik standardı (K-12) sayesinde cihazın tüm kişiliği merkezde tanımlıdır; yeniden imaj + kimlik enjeksiyonu cihazı eşdeğerine döndürür. 700 cihazda tek tek veri yedeği işletilemez. ALTERNATİF: Cihaz-başı tam yedek — bant genişliği ve operasyon yükü nedeniyle elendi. RİSK: MEG yerel verisi kaybolur; azaltma: MEG verisi gerekiyorsa merkezi yedek hedefi 13-MEG-ACCEPTANCE'ta tanımlanır, teknisyen L8 öncesi uyarılır. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Kurtarma merdiveni üç ilkeye dayanır: (1) erişim varsa sahaya gidilmez — 4 erişim kanalından (K-04) en az biri ayaktaysa onarım uzaktan yapılır; (2) config kodludur — IaC Git'te olduğu sürece merkez de saha da yeniden üretilebilir; (3) cihaz disposable'dır — veri cihazda değil merkezde yaşar. LEVEL 9 (merkez inşası) bu yüzden dosya yedeği + playbook ikilisine dayanır, pahalı imaj altyapısına değil. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Tek-seviye "sıfırla" | Basit anlatım | Her arızada saha ziyareti, veri kaybı | Elendi | | Sağlayıcı snapshot birincil | Hızlı geri dönüş | Taşınabilir değil, sağlayıcı kilidi | Yedek | | Cihaz-başı tam yedek (Borg/restic per-node) | Veri korunur | 700 cihazda bant/işletme yükü | Elendi (MEG verisi ayrı ele alınır) | | Clonezilla ile bit-kopya geri yükleme | Hızlı | Donanım-fragil (K-09) | Yedek yöntem | ## 6. Avantajlar - Her arızada "hangi seviye" sorusunun cevabı tabloda hazır; teknisyen doğaçlama yapmaz. - Merkezi kol sayesinde çoğu arıza saha ziyaretsiz çözülür. - L9 tatbikatı merkezin gerçekten yeniden kurulabildiğini kanıtlar (kağıt üstünde kalmaz). ## 7. Dezavantajlar - Merdiven disiplini eğitim ister; acemi teknisyen L1'i atlayıp doğrudan L8'e koşabilir. - IaC drift'i (Git ≠ gerçek) L9'u yavaşlatır; periyodik tatbikat şarttır. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Yanlış seviye seçimi, arızayı büyütür | Orta | Orta | Her seviyede "yükseltme kriteri" + runbook yönlendirmesi (20/21) | | Yedek alınmıyor ama alınıyor sanılıyor | Orta | Yüksek | Yedek doğrulama job'ı + Uptime Kuma/Prometheus alarmı | | L8'de bayi no yanlış girilir, kimlik çakışır | Düşük | Yüksek | 02-NAMING benzersizlik kontrolü + kurucu script onayı | | Merkez yedeği şifresiz saklanır | Düşük | Yüksek | Yedek şifreleme + erişim yalnızca merkezi admin (21) | ## 9. Uygulama Planı ### LEVEL tablosu (her seviyede Merkezi + Yerel kol) | LEVEL | Ad | Merkezi prosedür (uzaktan) | Yerel prosedür (sahada) | Üst seviyeye geç kriteri | |---|---|---|---|---| | L1 | Servis restart | Ansible/Semaphore ile `systemctl restart` (Docker, xrdp, wg-quick, agent) | `sudo systemctl restart ` | Servis 2 restartta ayağa kalkmıyorsa → L2 | | L2 | Reboot | `reboot` (Semaphore job veya SSH); boot zinciri izlenir (16) | Güç düğmesi / fiş-çek-tak (tek sefer) | Boot sonrası aynı arıza sürüyorsa → L3 | | L3 | Son değişikliği geri alma | 10-UPDATE rollback zinciri (önceki config/imaj) | Teknisyen değişikliği yaptıysa manuel geri alma (20) | Geri alma sonrası düzelmiyorsa → L4 | | L4 | Alternatif kanaldan onarım | WG çöktüyse RustDesk/MeshCentral ile bağlan, WG config'ini düzelt; tersi durumda SSH-over-WG ile agent düzelt | Teknisyen telefonla merkezden komut alır, uygular | Hiçbir kanaldan erişilemiyorsa → L5/L8 hazırlığı | | L5 | Config geri yükleme | Ansible ile bilinen-iyi config'i bas (sshd, wg0, daemon.json, exporter) | USB'deki `config/` klasöründen kopyalama (kurucu medyasında) | Config doğru ama servis kalkmıyorsa → L6 | | L6 | Docker/MEG yeniden kurulum | Playbook ile Docker + MEG stack yeniden kurulumu | `blueforce-install.sh --only-docker-meg` (varsa) | İmaj bozulmuş görünüyorsa → L7/L8 | | L7 | OS onarımı | Kırık paket/çekirdek onarımı (`apt --fix-broken`, eski çekirdekle boot) | Recovery mod + teknisyen adımları (20) | Kök dosya sistemi hasarlıysa → L8 | | L8 | USB ile sıfırdan imaj | Merkez L8 kararı verir, teknisyeni yönlendirir (21) | USB → Ubuntu → `blueforce-install.sh` → bayi no → READY (20) | Cihaz donanımsal arızalıysa → donanım değişimi; donanım sağlamsa L8 son duraktır | | L9 | Merkez VDS yeniden inşası | Boş VDS'e playbook/compose + dosya yedeğiyle kurulum; DNS/kimlikler güncellenir | — (teknisyen işi değil) | — | ### Yedeklenecekler ve sıklık 1. Merkez dosya yedeği (günlük, otomatik): Ansible inventory + `host_vars`/`group_vars`, WireGuard anahtarları (şifreli), Semaphore DB dump, Prometheus config + kurallar, Grafana dashboard JSON'ları, compose dosyaları, RustDesk keypair, MeshCentral config/DB. 2. IaC Git deposu (her değişiklikte commit+push): playbook'lar, roller, compose'lar, autoinstall/preseed, `blueforce-install.sh`. 3. Yedek doğrulama (aylık): en yeni yedeğin açılabilirliği + kritik dosya hash kontrolü; sonuç monitoring'e işlenir. ```bash # örnek: saha cihazı hızlı durum (L1 öncesi tanı) bf-status # örnek: tanılama paketi (secret sızdırmaz) bf-support-bundle --out /tmp/bf-12010193-bundle.tar.gz # örnek: merkezden tek cihazda servis restart (Ansible) ansible-playbook playbooks/restart.yml --limit bf-12010193 ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | L1→L2 merdiven tatbikatı (servis kill) | L1 çözer, kayıt düşer | LAB(2) | | WG config bozma → L4 (RustDesk ile onarım) | Tünel uzaktan onarılır | P1(5) | | L8 tam yeniden imaj + bayi no | READY < hedef süre | LAB(2) | | L9 boş VDS'e sıfırdan kurulum tatbikatı | Tüm servisler ayağa kalkar | İzole VDS | | Yedekten geri yükleme (Semaphore DB) | İş geçmişi ve envanter döner | Merkez test | ## 11. Rollback 1. Kurtarma müdahalesi arızayı büyütürse bir üst LEVEL'e geçilir, geri dönülmez (merdiven tek yönlüdür). 2. L3 geri alması yanlışsa 10-UPDATE zincirindeki önceki-önceki sürüme geçilir. 3. L9 inşası başarısızsa eski VDS (kapatılmadıysa) ayakta tutulur; DNS geri alınır. ## 12. Kontrol Listesi - [ ] L1…L9 tablosu 20 (teknisyen) ve 21 (merkez) runbook'larına linkli. - [ ] Yedek listesi + sıklığı merkezde job'a bağlı, doğrulaması alarmlı. - [ ] IaC deposu güncel; son L9 tatbikat tarihi yazılı. - [ ] `bf-support-bundle` secret dışlama kuralı testli (K-14). ## 13. Açık Sorular - [ ] Yedek saklama süresi ve şifreleme anahtarının kasası (sahibi: merkezi admin, 21). - [ ] MEG yerel veri yedeği gerekli mi — 13-MEG kararı bekleniyor (sahibi: Faz 3). - [ ] L9 tatbikat periyodu (öneri: 3 ay) onayı (sahibi: 25-ROADMAP). --- ## Ek: Mermaid — kurtarma merdiveni ```mermaid flowchart TB F["Arıza bildirimi
(teknisyen / monitoring)"] --> L1["L1 servis restart"] L1 -->|düzelmedi| L2["L2 reboot"] L2 -->|düzelmedi| L3["L3 son değişikliği geri al"] L3 -->|düzelmedi| L4["L4 alternatif kanaldan onarım
(WG ↔ RustDesk/MeshCentral)"] L4 -->|erişim yok| L8["L8 USB ile sıfırdan imaj"] L4 -->|erişim var, config bozuk| L5["L5 config geri yükleme"] L5 -->|düzelmedi| L6["L6 Docker/MEG yeniden kurulum"] L6 -->|düzelmedi| L7["L7 OS onarımı"] L7 -->|düzelmedi| L8 L8 --> OK["READY"] L9["L9 merkez VDS inşası
(ayrı tetikleyici)"] --> OKC["Merkez servisler ayakta"] ```
# 18 — Pilot ve 700 Cihaz Yayılımı (Pilot & Rollout) > Kısa özet: Yayılımın ilk adımı `bf-hardware-inventory.sh` donanım taramasıdır; imaj dondurulmadan önce saha heterojenliği ölçülür. Sonra LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD dalgaları, her dalgada giriş/çıkış kriteriyle ilerler. - Dosya: `docs/18-PILOT-AND-700-DEVICE-ROLLOUT.md` - İlgili kararlar: `24-DECISION-LOG.md#K-01` (donanım profili), `#K-09` (önce envanter, sonra imaj), `#K-11` (dalga zinciri), `#K-12` (cihaz kimliği) - Durum: [ ] Taslak --- ## 1. Amaç 700 cihazı "bir gecede" değil, ölçülebilir dalgalarla üretime almak. Her dalga bir öncekinin kanıtı üzerine kurulur; bir dalga batarsa filo değil, o dalga etkilenir. ## 2. Kapsam - Kapsam içi: donanım envanter taraması, dalga tanımı ve sayıları, her dalganın giriş/çıkış kriterleri, dalga başına izlenecek metrikler, durdurma (halt) kuralları. - Kapsam dışı: update'lerin dalga zinciri (10-UPDATE — aynı dalga isimlerini kullanır ama tetikleyici farklıdır), tek cihaz kurulum adımları (20-FIELD-RUNBOOK). ## 3. Kararlar ```text KARAR: İmaj dondurulmadan ÖNCE `bf-hardware-inventory.sh` ile saha donanım taraması yapılır; CPU/RAM/disk/NIC/GPU profilleri çıkarılmadan golden image dondurulmaz. GEREKÇE: K-09'un en büyük riski heterojen donanımda sürücü eksiğidir; tarama olmadan üretilen tek imaj sahada rastgele kırılır. Profil listesi LAB test matrisini belirler. ALTERNATİF: Taramasız "standart PC varsayımı" — ilk dalgada sürpriz donanımla çakışır; elendi. RİSK: Tarama örneklemi yanlı olur (kolay ulaşılan bayiler); azaltma: bölge başına kota + en az %10 örneklem hedefi. MALİYET: Ücretsiz (script + Ansible). LİSANS: Yok. ``` ```text KARAR: Dalgalar: LAB(2) → P1/PILOT-1(5) → P2/PILOT-2(20) → W1/WAVE-1(50) → W2/WAVE-2(100) → PROD (kalan ~523). Hiçbir dalga, önceki dalganın çıkış kriterini sağlamadan başlamaz. GEREKÇE: K-11'in onay zinciri; üstel büyüme (2→5→20→50→100→523) riski kademeli taşır, her adımda gözetim süresi vardır. ALTERNATİF: Tek seferde toplu kurulum — geri dönüşü olmayan filo-çapı hata riski; elendi. RİSK: Takvim baskısıyla kriter atlanır; azaltma: dalga geçişi merkezi admin onayı gerektirir (21), Semaphore'da kapı adımı. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Önce ölç (envanter), sonra dondur (imaj), sonra büyüt (dalgalar). Bu sıra, 700 cihazda "bilinmeyen donanım" ve "bilinmeyen ölçek" risklerini birbirinden ayırır: donanım riski LAB'da, ölçek riski dalgalarda eritilir. Dalga sayıları (2/5/20/50/100) hem K-11'deki update zinciriyle aynıdır hem de teknisyen kapasitesine uyar (erken dalgalar az cihazla çok öğrenme). ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Taramasız imaj | Hızlı başlangıç | Sahada sürücü sürprizi | Elendi | | Tek dalga (big-bang) | Hızlı | Filo-çapı geri dönüşsüz hata | Elendi | | Daha fazla küçük dalga (10+) | Çok güvenli | Takvim ve teknisyen yorgunluğu | Elendi (6 dalga yeterli) | ## 6. Avantajlar - Her dalga, sonraki dalganın eğitim verisidir (süre, hata tipi, destek yükü ölçülür). - Durdurma kuralları sayesinde kötü imaj 5 cihazdan öteye geçemez. - Envanter taraması aynı zamanda 02-INVENTORY'nin veri kaynağıdır (iki iş bir taş). ## 7. Dezavantajlar - Dalgalar takvimi uzatır; yönetim "neden hepsi birden değil" diye sorar (bu doküman cevaptır). - Erken dalga bayileri pilot yükü taşır; iletişim ve destek planı gerekir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Envanter örneklemi yanlı | Orta | Yüksek | Bölge kotası + %10 hedefi | | Kriter atlanarak dalga atlama | Orta | Yüksek | Merkezi admin onayı zorunlu (21) | | Pilot bayilerde destek yığılması | Orta | Orta | Dalga başına teknisyen kapasite planı (25) | | Donanım profili imajı kırar (yeni parti PC) | Orta | Orta | Her yeni donanım partisi LAB'a girer | ## 9. Uygulama Planı ### FAZ 0 — Donanım taraması (her şeyden önce) 1. `bf-hardware-inventory.sh` mevcut/örnek cihazlarda çalıştırılır (CPU, RAM, disk, NIC, GPU, BIOS modu, mevcut OS). 2. Çıktılar merkezde toplanır, donanım profilleri çıkarılır (örn. Profil-A: i3/4GB/120GB, Profil-B: i5/8GB/256GB…). 3. Profil listesi 04-GOLDEN-IMAGE'a girer; LAB test matrisi = her profilden ≥1 cihaz. 4. Profil başına sürücü notu düşülür (Wi-Fi çipi, ekran kartı, yazıcı). ```bash # örnek: envanter taraması (sahada veya Ansible ile) sudo ./bf-hardware-inventory.sh --out /tmp/inv-$(hostname).json # örnek: merkezde toplama ansible all -m fetch -a "src=/tmp/inv-{{ inventory_hostname }}.json dest=inventory/hw/ flat=yes" ``` ### Dalga planı | Dalga | Cihaz | Amaç | Gözetim süresi | Çıkış kriteri (hepsi şart) | |---|---|---|---|---| | LAB | 2 | İmaj + 3 akış doğrulama (erişim/boot/update) | 1 hafta | 3 akış yeşil, support-bundle temiz, rollback denendi | | P1 (PILOT-1) | 5 | Gerçek saha + WG kopma sayacı + teknisyen akışı | 2 hafta | Kopma sayacı toplandı, teknisyen USB→READY'yi desteksiz yaptı, kritik hata yok | | P2 (PILOT-2) | 20 | Ölçek provası + digest pinleme değerlendirmesi + destek yükü ölçümü | 2–3 hafta | Cihaz-başı kurulum süresi hedefte, destek bileti/cihaz eşiği altında, monitoring 17 metrik dolu | | W1 (WAVE-1) | 50 | Bölge-ölçeği yayılım, envanter disiplini | 3 hafta | Envanter %100 eşleşiyor, halt kuralı tetiklenmedi | | W2 (WAVE-2) | 100 | Üretim-öncesi son prova, paralel ekip çalışması | 3–4 hafta | İki ekip paralel kurulum yapabiliyor, geri alma zinciri canlı denendi | | PROD | ~523 | Kalan filo | planlı | W2 çıkış kriterleri + yönetim onayı | ### Halt (durdurma) kuralları 1. Dalga içinde aynı kök-nedenle ≥2 kritik arıza → dalga durur, 17-RECOVERY L3 işletilir. 2. Monitoring'de dalga cihazlarının >%10'u 24 saatte çevrimdışı → yayılım durur, ağ/VPN incelenir. 3. Teknisyen kurulum süresi hedefin 2× üstünde → eğitim/runbook (20) revize edilmeden devam edilmez. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Envanter scripti 3 farklı donanımda | Profil JSON'u eksiksiz | LAB + örnek saha | | LAB 3-akış testi | Erişim + boot + update yeşil | LAB(2) | | P1 kopma sayacı 7/24 | Keepalive ayarı için veri | P1(5) | | Dalga geçiş kapısı (Semaphore) | Onaysız geçiş engellenir | Merkez test | ## 11. Rollback 1. Dalga içi arızada etkilenen cihazlar 17-RECOVERY merdiveniyle (L3/L8) önceki-iyi duruma alınır. 2. İmaj-köklü arızada dalga durdurulur, imaj revize edilir, dalga baştan başlar (ilerideki dalgalar etkilenmez). 3. PROD'da imaj sorunu çıkarsa 10-UPDATE rollback zinciri işletilir. ## 12. Kontrol Listesi - [ ] Envanter taraması tamam, profil listesi 04'e işlendi. - [ ] Her dalganın giriş/çıkış kriteri + gözetim süresi yazılı ve onaylı. - [ ] Halt kuralları 21 (merkez) runbook'una işlendi. - [ ] Dalga cihaz listeleri envanterde (`BF-`) ayrıldı. ## 13. Açık Sorular - [ ] Örneklem hedefi (%10) ve bölge kotaları onayı (sahibi: merkezi admin). - [ ] Pilot bayi seçimi + iletişim planı (sahibi: 25-ROADMAP). - [ ] Cihaz-başı kurulum süresi hedefi (öneri P2'de ölçülüp sabitlenecek). --- ## Ek: Mermaid — dalga akışı ```mermaid flowchart LR INV["Donanım taraması
bf-hardware-inventory.sh"] --> PROF["Profil listesi"] PROF --> IMG["Golden image
(04)"] IMG --> LAB["LAB 2"] LAB -->|çıkış kriteri| P1["P1 5"] P1 -->|çıkış kriteri| P2["P2 20"] P2 -->|çıkış kriteri| W1["W1 50"] W1 -->|çıkış kriteri| W2["W2 100"] W2 -->|çıkış + onay| PROD["PROD ~523"] LAB -->|halt| FIX["Düzelt +
tekrar"] P1 -->|halt| FIX P2 -->|halt| FIX W1 -->|halt| FIX W2 -->|halt| FIX FIX --> LAB ```
# 19 — Sorun Giderme (Troubleshooting) > Kısa özet: Sahada ve merkezde en sık görülen arızaların belirti → neden → komut → çözüm tablosu. 17-RECOVERY "hangi seviye" sorusunu, bu doküman "o seviyede hangi komut" sorusunu cevaplar. - Dosya: `docs/19-TROUBLESHOOTING.md` - İlgili kararlar: `24-DECISION-LOG.md#K-03` (xRDP/GUI), `#K-04` (erişim kanalları), `#K-05` (WireGuard), `#K-10` (Docker), `#K-14` (`bf-status`, support-bundle) - Durum: [ ] Taslak --- ## 1. Amaç Tekrar eden arızalarda teşhisi hızlandırmak: belirtiyi bulan teknisyen/admin, tabloya bakıp ilk komutu 30 saniyede çalıştırır. Her girdide "ne zaman üst seviyeye çık" notu vardır. ## 2. Kapsam - Kapsam içi: boot, ağ/VPN, SSH, RDP/GUI, RustDesk, MeshCentral, Docker/MEG, disk, monitoring belirti tabloları. - Kapsam dışı: seviye kararları (17), adım-adım kurulum (20), merkez onay akışı (21). ## 3. Kararlar ```text KARAR: Her arıza girdisi aynı formatta yazılır: Belirti → Olası nedenler (olasılık sırasıyla) → Tanı komutları → Çözüm → Yükseltme kriteri. GEREKÇE: Standart format, acemi teknisyenin bile doğru sırayla (tanıdan önce çözüm yok) ilerlemesini sağlar; her çözüm denenebilir komutla verilir. ALTERNATİF: Serbest-anlatım sorun listesi — komutlar dağınık kalır, otomasyona çevrilemez; elendi. RİSK: Tablo eskirse yanlış yönlendirir; azaltma: her PİLOT dalgası sonrası tablo gözden geçirilir. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Sorun giderme, kurtarma merdiveninin (17) "içini" doldurur: 17 hangi LEVEL, 19 o LEVEL'de hangi komut. Olasılık sırası önemlidir — en yaygın neden önce yazılır, teknisyen zamanı boşa harcanmaz. Yükseltme kriteri her girdinin sigortasıdır (inatla aynı komutu tekrarlamak yasak). ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Harici bilgi tabanı (Wiki araması) | Aranabilir | Çevrimdışı sahada erişilemez, Git akışı yok | Elendi (22'de Docusaurus içinde bu dosya zaten aranabilir) | | Her arıza için ayrı sayfa | Detaylı | 700 cihazda 100 sayfa yönetilemez | Elendi (gerekirse Ek'e taşınır) | ## 6. Avantajlar - Tek dosya, çevrimdışı da okunur (USB'de veya basılı). - Komutlar kopyala-yapıştırılabilir; açıklamalar Türkçe, komut adları İngilizce orijinal. ## 7. Dezavantajlar - 26.04.1'e özgü çıktı farkları LAB'da güncellenmelidir (kesin servis adları, log yolları). - MEG'e özgü arızalar 13-MEG-ACCEPTANCE'tan beslenmelidir; bu dosya iskelettir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Eski komut (servis adı değişti) | Orta | Orta | LAB'da her komut çalıştırılıp doğrulanır | | Secret'lı komut geçmişe düşer | Düşük | Yüksek | Support-bundle kuralı (K-14); komutlarda secret yazılmaz | ## 9. Uygulama Planı Önce genel tanı (her arızada ilk 60 saniye): ```bash bf-status systemctl --failed ip -brief addr; ip route show default wg show wg0 2>&1 | head -20 docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.RestartPolicy}}' df -h / /var/lib/docker | cat ``` ### T-1 — Cihaz açılmıyor / ekranda hiçbir şey yok | # | Neden (olasılık sırası) | Tanı | Çözüm | |---|---|---|---| | 1 | Elektrik yok / BIOS güç ayarı | Priz, adaptör, güç LED'i | BIOS "Restore on AC Power Loss = Power On" (16) | | 2 | Monitör/kablo | Başka kablo/ekran dene | Değiştir | | 3 | Disk ölmüş | BIOS diski görmüyor | Donanım değişimi → L8 yeni diskte | Yükseltme: donanım sağlamsa ama OS açılmıyorsa T-2'ye geç. ### T-2 — Boot terminale düşmüyor / servisler kalkmıyor | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | Failed servis var | `systemctl --failed`, `journalctl -xb -p err` | İlgili servis girdisine git (T-3…T-6) | | 2 | Disk dolu | `df -h /` | Docker log temizliği + rotasyon kontrolü (T-7) | | 3 | Bozuk çekirdek/paket | `dpkg --audit`, `apt --fix-broken install` | 17-L7; olmazsa L8 | ### T-3 — WireGuard tüneli yok (`wg show` boş / handshake eski) | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | Servis inaktif | `systemctl status wg-quick@wg0` | `sudo systemctl enable --now wg-quick@wg0` | | 2 | Anahtar/config hatası | `journalctl -u wg-quick@wg0 --no-pager`, `wg show wg0 latest-handshakes` | Config'i merkezden yeniden bas (17-L5); peer adı `bf-` mi kontrol et | | 3 | Saha interneti yok / CGNAT | `ping -c3 8.8.8.8`, modem arayüzü | Yerel ağ düzeltilir; kopma sayacı not edilir (P1 görevi) | | 4 | Merkez hub down | Başka cihazdan hub'a bak | 17-L9 kolu işletilir | Yükseltme: WG kalkmıyorsa RustDesk/MeshCentral ile devam (17-L4); onlar de yoksa saha ziyareti. ### T-4 — SSH bağlanmıyor (WG ayaktayken) | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | sshd down / anahtar eşleşmiyor | `systemctl status ssh`, `ssh -vvv blueforce@bf-` | Anahtarı merkezden yeniden dağıt (K-13); password denemesi YOK | | 2 | Yanlış IP/hostname | `ip -brief addr`, envanter karşılaştır | 02-NAMING'e göre düzelt | | 3 | Güvenlik duvarı | `sudo ufw status`, `ss -tlnp` | 11-HARDENING standardına döndür | ### T-5 — RDP açılmıyor / siyah ekran / hemen kopuyor | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | GUI katmanı kapalı | `systemctl status display-manager xrdp xrdp-sesman` | `bf-gui-on`, tekrar dene | | 2 | Oturum çakışması (yerel + RDP aynı kullanıcı) | `who`, `loginctl list-sessions` | Ayrı kullanıcı/oturum; eski oturumu kapat | | 3 | xRDP config bozulmuş | `journalctl -u xrdp --no-pager` | Bilinen-iyi config'i bas (17-L5) | ### T-6 — RustDesk bağlanmıyor | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | İstemci servis/uygulama kapalı | RustDesk durumu, ID görünüyor mu | Servisi başlat; ID+röle adresi merkez değerlerle aynı mı | | 2 | Key uyuşmazlığı | İstemci key vs hbbs key | Merkezi key'i yeniden bas (07-REMOTE-ACCESS) | | 3 | hbbs/hbbr down | Merkezden konteyner/servis durumu | Merkez tarafını ayağa kaldır; health monitörü (Kuma) ne diyor | ### T-7 — Docker/MEG kalkmıyor veya sürekli restart | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | Restart loop (config/entrypoint) | `docker ps`, `docker logs --tail 100 ` | Config düzelt + `docker compose up -d` (17-L6) | | 2 | Disk dolu (rotasyonsuz log) | `df -h /var/lib/docker`, `du -sh /var/lib/docker/containers/*` | `daemon.json` rotasyonu uygula; taşan logu buda | | 3 | `latest` çekilmiş / sürüm kaymış | `docker images`, compose tag kontrolü | Sabit tag'e döndür (K-10); Watchtower varsa kaldır | | 4 | Port çakışması / UFW bypass | `ss -tlnp`, `docker port ` | `127.0.0.1`-bind standardına döndür (K-10) | ### T-8 — Disk doluyor | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | Docker json-file logları | `du -sh /var/lib/docker/containers/*` | Rotasyon + budama, `docker system prune` (dikkatli) | | 2 | journald şişmiş | `journalctl --disk-usage` | `SystemMaxUse` limiti + `journalctl --vacuum-size=` | | 3 | Prometheus retention (merkez) | `df -h`, retention ayarı | 30–90 gün bandına çek (K-07) | ### T-9 — Monitoring'de cihaz gri/kayıp | # | Neden | Tanı | Çözüm | |---|---|---|---| | 1 | Node Exporter down | `systemctl status node_exporter`, hedef scrape logu | Servisi başlat; textfile dizin izni | | 2 | Uptime Push gelmiyor | Kuma "last seen" | Cihaz interneti + push job'ı (T-3'e dön) | | 3 | Etiket yanlış (`BF-`) | Prometheus target etiketleri | Envanterle eşitle (02) | ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Her T girdisindeki ilk tanı komutu LAB'da çalışır | Komut + örnek çıktı bu dosyada | LAB(2) | | WG-kill → T-3 → L4 onarım tatbikatı | < hedef sürede tünel döner | P1(5) | | Disk-doldur simülasyonu → T-8 | Alarm + temizlik çalışır | LAB(2) | ## 11. Rollback Yanlış çözüm uygulandıysa 17-L3 (son değişikliği geri alma) işletilir; tanı komutlarının kendisi read-only'dir, rollback gerektirmez. ## 12. Kontrol Listesi - [ ] Her girdide Yükseltme kriteri var. - [ ] Komutların tamamı LAB 26.04.1 imajında denendi. - [ ] Hiçbir girdide secret/şifre yazılmıyor. - [ ] MEG'e özgü satırlar 13 ile çapraz linkli. ## 13. Açık Sorular - [ ] Kesin `bf-gui-*` birim adları + GNOME oturum satırı — LAB çıktısı (sahibi: Faz 3). - [ ] MEG log hacmi ölçümü → T-8 eşikleri (sahibi: 12-DOCKER yazarı). - [ ] Turkcell CGNAT tanı notları — P1 verisi (sahibi: 08-WIREGUARD yazarı). --- ## Ek: Triyaj Akışı (hangi T girdisi?) ```mermaid flowchart TB SORUN["Arıza bildirimi
bf-status + systemctl --failed"] --> ACILIR{"Cihaz açılıyor mu?"} ACILIR -->|Hayır| T1["T-1: güç / BIOS / disk"] ACILIR -->|Evet| BOOTOK{"Servisler kalkıyor mu?"} BOOTOK -->|Hayır| T2["T-2: failed servis / disk dolu"] BOOTOK -->|Evet| WG{"wg show handshake güncel mi?"} WG -->|Hayır| T3["T-3: WireGuard + L4 yedek kanal"] WG -->|Evet| SSH{"SSH oluyor mu?"} SSH -->|Hayır| T4["T-4: sshd / anahtar / UFW"] SSH -->|Evet| RDP{"RDP açılıyor mu?"} RDP -->|Hayır| T5["T-5: bf-gui-on / xRDP"] RDP -->|Evet| DERIN["T-6+: Docker / MEG / disk (T-7/T-8)"] ```
# 20 — Saha Teknisyeni Runbook > Kısa özet: Sahada cihaz başına giden teknisyenin el kitabı. Tek akış: USB'yi tak → kur → bayi numarasını gir → READY'yi gör → çık. Komut ezberi istenmez; her adımda "ne göreceksin, ne yazacaksın" vardır. - Dosya: `docs/20-FIELD-TECHNICIAN-RUNBOOK.md` - İlgili kararlar: `24-DECISION-LOG.md#K-09` (USB → script → bayi no → READY), `#K-12` (bayi no → `BF-`), `#K-14` (`bf-status`, support-bundle) - Durum: [ ] Taslak --- ## 1. Amaç Teknisyenin sahada tek başına, telefonsuz bile cihazı READY hale getirmesi. Bu dosya "neden" anlatmaz, "ne yap" söyler. Neden merak eden 01'e bakar. ## 2. Kapsam - Kapsam içi: USB ile sıfır kurulum, bayi no girişi, READY kontrolü, arızalı cihazda ilk 3 hareket, merkeze ne zaman telefon açılacağı. - Kapsam dışı: merkez onayları (21), arıza teşhis tablosu detayı (19 — sadece yönlendirme), update dalgaları (10). ## 3. Kararlar ```text KARAR: Teknisyen akışı 4 adımdır: USB → kur → bayi no → READY. Teknisyen hostname, IP, anahtar, config dosyası ile uğraşmaz; hepsini `blueforce-install.sh` yapar. GEREKÇE: K-09'un hedef akışı; insan hatası en çok elle config'de olur. Teknisyenin tek manuel girdisi 8 haneli bayi numarasıdır (K-12). ALTERNATİF: Teknisyene tam Linux eğitimi + manuel kurulum — 700 cihazda standardizasyon bozulur; elendi. RİSK: Script USB'si eski sürüm kalır; azaltma: USB etiketinde sürüm + merkezden "şu sürümden eski USB'yi kullanma" duyurusu. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Teknisyenin işi "bilgisayarcılık" değil "bayi cihazını çalışır teslim etmek"tir. Karar, teknisyenin dikkatini tek kritik noktaya toplar: bayi numarasını doğru girmek. Geri kalan her şey script + merkez otomasyonudur. Bu yüzden bu dosya kısa cümlelerle, emir kipiyle yazılır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Detaylı Linux kılavuzu | Bilgili teknisyen | Uzun, hata açık, herkes okumaz | Elendi | | Merkezden uzaktan kurulum (teknisyensiz) | Saha maliyeti yok | İlk kurulumda cihazda internet/VPN yok | Elendi (ilk kurulum sahada) | ## 6. Avantajlar - Yeni teknisyen 1 saatlik eğitimle sahaya çıkar. - Her cihaz aynı adımla kurulur; "ustalık farkı" kapanır. ## 7. Dezavantajlar - Script'in yapmadığını teknisyen de yapamaz; olağandışı durumda merkeze bağımlıdır (bilerek böyle). - USB medyası fiziksel lojistik ister (sürüm takibi). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Bayi no yanlış girilir | Orta | Yüksek | Script ekranda `BF-`'yu gösterir, teknisyen bayiye onaylatır | | Eski USB ile kurulum | Orta | Orta | USB sürüm etiketi + kurucu açılışta sürümü söyler | | READY görülmeden sahadan çıkılır | Düşük | Yüksek | Kontrol listesi imzalanmadan iş kapanmaz | ## 9. Uygulama Planı ### A. Sıfır kurulum (yeni cihaz) — 4 adım **ADIM 1 — USB'yi tak, bilgisayarı aç.** 1. Kurulum USB'sini tak. 2. Bilgisayarı aç, USB'den başlat (açılışta genelde F12/F8/F2 — ekranda yazar). 3. Ubuntu kurulum ekranı gelene kadar bekle. > Göreceğin: Ubuntu kurulum ekranı. Bunu görmüyorsan: USB tam takılı mı, BIOS boot sırası USB mi? Olmazsa merkeze telefon aç. **ADIM 2 — Ubuntu'yu kur.** 1. Ekrandaki kurulumu varsayılanlarla tamamla (dil: Türkçe, disk: tamamını kullan). 2. Kurulum bitince USB'yi çıkarma, bilgisayarı yeniden başlat. **ADIM 3 — Kurucu scripti çalıştır, bayi numarasını gir.** 1. Terminali aç, şunu yaz: ```bash sudo ./blueforce-install.sh ``` 2. Script senden **8 haneli bayi numarasını** ister. Faturadaki/sistemdeki numarayı yaz, Enter'a bas. Örnek: `12010193`. 3. Script ekranda şunu gösterecek: ```text Device ID: BF-12010193 Hostname: bf-12010193 ``` 4. Numarayı bayiye sesli onaylat: "Cihaz numaranız BF-12010193, doğru mu?" Yanlışsa scriptten çık, tekrar çalıştır. **ADIM 4 — READY'yi gör, çık.** 1. Script bitene kadar bekle (kapatma, fişi çekme). 2. Sonunda şunu çalıştır: ```bash bf-status ``` 3. Ekranda **READY** (veya tüm satırlar yeşil/OK) görmelisin. 4. Kontrol listesini doldur (§12), sahadan çık. ### B. Arızalı cihaza gittin — ilk 3 hareket 1. `bf-status` çalıştır. Kırmızı satır hangisi? Not al. 2. Bilgisayarı bir kez yeniden başlat. Düzelirse READY'yi gör, çık. 3. Düzelmezse support paketini al, merkeze gönder: ```bash bf-support-bundle --out /tmp/bf--bundle.tar.gz ``` Paketi merkeze ilet (telefonla nasıl göndereceğin söylenir), merkez sana söylemeden başka komut çalıştırma. ### C. Merkeze telefon açma kuralları Hemen ara: READY alamıyorsan, aynı arıza 2 cihazda üst üste çıktıysa, donanım kırık/yanık kokusu varsa, bayi numarasından emin değilsen. Arama: cihaz başında ol, `BF-`'yu ve `bf-status` çıktısını hazır tut. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Sıfır teknisyen 4 adımı desteksiz yapar | READY < hedef süre | LAB(2) | | Yanlış bayi no girilir | Script uyarır, kuruluma devam etmez | LAB(2) | | Elektrik kesintisi simülasyonu sonrası `bf-status` | READY döner | LAB(2) | ## 11. Rollback Teknisyen rollback yapmaz. Kurulum yarıda kesildiyse: baştan başla (ADIM 1). İkinci denemede de olmazsa merkeze telefon aç — merkez 17-RECOVERY'den seviyeyi seçer. ## 12. Kontrol Listesi - [ ] USB sürümü güncel (etiket + merkez duyurusu eşleşiyor). - [ ] `BF-` bayiye onaylatıldı. - [ ] `bf-status` → READY görüldü. - [ ] Cihaz fişe + internete bağlı bırakıldı. - [ ] İş formu dolduruldu (bayi no, tarih, teknisyen adı). ## 13. Açık Sorular - [ ] Kurulum USB'sinin dağıtım/sürüm takip yöntemi (sahibi: merkezi admin, 21). - [ ] İş formu kağıt mı mobil mi (sahibi: 25-ROADMAP). - [ ] Hedef kurulum süresi (öneri: P2'de ölçülecek). --- ## Ek: Mermaid — teknisyen akışı ```mermaid flowchart TB USB["USB tak → aç"] --> KUR["Ubuntu kur"] KUR --> SCR["sudo ./blueforce-install.sh"] SCR --> NO["Bayi no gir
(8 hane)"] NO --> ONAY["BF-no'yu bayiye onaylat"] ONAY --> BEKLE["Script bitsin"] BEKLE --> READY["bf-status → READY?"] READY -->|evet| CIK["Formu doldur → çık"] READY -->|hayır| TEL["Merkeze telefon aç"] ```
# 21 — Merkezi Yönetici Runbook > Kısa özet: Merkezdeki yöneticinin el kitabı: hangi işlemi kim onaylar, update'i kim başlatır, dalga kapıları nasıl açılır, teknisyen ne zaman sahaya sürülür. Yetki ve sıra bu dosyadadır. - Dosya: `docs/21-CENTRAL-ADMIN-RUNBOOK.md` - İlgili kararlar: `24-DECISION-LOG.md#K-06` (Ansible/Semaphore), `#K-11` (onay zinciri + dalgalar), `#K-13` (anahtar dağıtımı) - Durum: [ ] Taslak --- ## 1. Amaç Merkezde "kim, neyi, hangi sırayla yapar" sorusunu kapatmak. Update'i kim başlatır, dalgayı kim onaylar, acil durumda kim karar verir — hepsi burada yazar, kriz anında aranmaz. ## 2. Kapsam - Kapsam içi: rol ve onay yetkileri, update başlatma akışı, dalga kapı prosedürü, anahtar/erişim yönetimi, acil durum yetkisi, günlük/haftalık rutinler. - Kapsam dışı: tek cihaz kurulum adımları (20), komut-seviyesi teşhis (19), kurtarma seviye tablosu (17 — buradan çağrılır). ## 3. Kararlar ```text KARAR: Update'i yalnızca merkezi yönetici başlatır; teknisyen, bayi veya otomatik zamanlayıcı update tetikleyemez. Her dalga geçişi açık onay (Semaphore kapı adımı + kayıt) gerektirir. GEREKÇE: K-11 "onaysız update yasak" ilkesinin operasyonel karşılığıdır; tek başlatma noktası = denetlenebilirlik. Otomatik tetikleyici olmaması, zamanlanmış sürprizleri engeller. ALTERNATİF: Takvimli otomatik dalga başlatma — onay kaydı zayıflar, arızada "kim başlattı" belirsizleşir; elendi. RİSK: Yönetici izindeyken update kilitlenir; azaltma: en az 2 onay yetkilisi tanımlanır (asıl + vekil). MALİYET: Ücretsiz. LİSANS: Yok. ``` ```text KARAR: Acil güvenlik update'inde dalga sırası atlanamaz ama gözetim süreleri kısaltılabilir; kısaltma kararı ve gerekçesi kayda geçirilir. GEREKÇE: Sıra atlamak (LAB'ı geçmeden PROD'a çıkmak) filo-çapı riskidir; süre kısaltmak ise bilinçli hızlandırmadır. Kayıt, sonradan denetimi sağlar. ALTERNATİF: Acilde direkt PROD — geri dönüşü olmayan hata riski; elendi. RİSK: "Acil" etiketi suistimal edilir; azaltma: acil tanımı (CVSS eşiği / aktif sömürü) 10-UPDATE'ta yazılıdır. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? 700 cihazda en tehlikeli cümle "ben başlattım sandım"dır. Başlatma yetkisini tek role toplamak, hem hatayı hem suçu önler: hata olursa kayıttan bulunur, suçlama olursa kayıt konuşur. Vekil kuralı, tek kişiye bağımlılığı kaldırır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Herkes update başlatabilir | Hızlı | Onaysız yayılım, denetimsizlik | Elendi | | Tam otomatik pipeline | Eforsuz | Politika ihlali (K-11) | Elendi | | Tek onay yetkilisi | Net | İzin/hastalıkta kilit | Elendi (asıl+vekil) | ## 6. Avantajlar - Her update'in "kim başlattı, kim onayladı, ne zaman" kaydı vardır. - Acil durumda bile sıra korunur; hız, sıranın içinde ayarlanır. ## 7. Dezavantajlar - Onay adımı update'i yavaşlatır (bilerek; hız 25-ROADMAP'ta acil yoluyla dengelenir). - Semaphore kapı adımı atlanırsa politika kağıtta kalır — teknik zorunluluk şarttır. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Kapı adımı bypass edilir (CLI'dan direkt run) | Orta | Yüksek | Semaphore dışı prod run'ı yasaktır; denetim logu izlenir | | İki yetkili de ulaşılamaz | Düşük | Orta | Acil iletişim zinciri + önceden tanımlı duraklatma (devam etme, durdur) | | Anahtar sızıntısı | Düşük | Yüksek | Per-admin anahtar iptali + rotasyon (K-13) | ## 9. Uygulama Planı ### Roller | Rol | Kim | Yetkisi | |---|---|---| | Onay yetkilisi (asıl + vekil) | İsimle tanımlanır | Dalga başlatma/onay, acil kısaltma kararı, L8/L9 kararı | | Operatör | Merkez ekibi | Onaylanmış job'u çalıştırma, monitoring izleme, teknisyen yönlendirme | | Teknisyen | Saha ekibi | 20'deki akış; update/dalga başlatamaz | | İzleyici (read-only) | Yönetim | Panoları görür, işlem yapamaz | ### Standart update akışı (yönetici gözüyle) 1. Update talebi gelir (güvenlik bülteni / MEG sürümü / OS yaması) → etki + geri alma planı raporlanır. 2. Onay yetkilisi LAB'ı başlatır: Semaphore'da job + kayıt. 3. LAB çıkış kriteri tuttuysa P1 → P2 → W1 → W2 → PROD, her kapıda açık onay. 4. Herhangi bir dalgada halt kuralı (18) tetiklenirse: yayılım DURUR, 17-L3 işletilir, kök neden bulunmadan kapı açılmaz. 5. PROD bitiminde kapanış kaydı (başarı/arıza sayısı, süre). ```bash # örnek: onaylı dalga run (operatör, onay sonrası) ansible-playbook playbooks/update.yml --limit wave_p1 # örnek: kapı kontrolü — onaysız PROD engeli (ilke; teknik karşılığı Semaphore'da) # PROD limiti yalnızca "prod-onay" etiketli job ile koşar ``` ### Anahtar ve erişim yönetimi 1. Yeni admin: anahtarı üretilir, `authorized_keys`'e Ansible ile dağıtılır (K-13); eski/ayrılan adminin anahtarı aynı yolla kaldırılır. 2. Paylaşılan tek private key YOKTUR; böyle bir dosya görülürse olay kaydı açılır. 3. Semaphore yalnızca WireGuard arkasından erişilir; gizliler yerleşik şifreli Key Store'dadır (K-06). ### Günlük / haftalık rutinler - Günlük (operatör, 10 dk): Uptime Kuma UP/DOWN, Prometheus `up` == 0 listesi, gece reboot olanlar, yedek job durumu. - Haftalık (yönetici, 30 dk): dalga takvimi, açık halt'lar, anahtar/envanter uyumsuzlukları, L9 tatbikat sayacı. ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | Onaysız PROD run denemesi | Engellenir + log düşer | Merkez test | | Semaphore kapalıyken Ansible CLI run | Onaylı iş CLI'dan koşar (K-06) | LAB(2) | | Vekil ile dalga onayı | Kayıtta vekil adı görünür | Merkez test | ## 11. Rollback 1. Hatalı dalga: 17-L3 + 10-UPDATE rollback zinciri; kapı bir sonraki dalgaya kapatılır. 2. Hatalı onay: aynı yetkili (veya vekil) durdurma kararı verir; "devam" kararı verilemez, yalnızca "dur + geri al". ## 12. Kontrol Listesi - [ ] Asıl + vekil isimle tanımlı ve iletişim bilgileri güncel. - [ ] Semaphore'da PROD kapı adımı teknik olarak zorunlu. - [ ] Her dalga onayı kayıtlı (kim, ne zaman, hangi sürüm). - [ ] Ayrılan personelin anahtarı kaldırıldı. ## 13. Açık Sorular - [ ] Acil tanımı eşikleri (CVSS/aktif sömürü) — 10-UPDATE ile ortak onay (sahibi: yönetici). - [ ] Kapanış raporu formatı ve saklama süresi (sahibi: 25-ROADMAP). - [ ] Read-only yönetim panosu kapsamı (sahibi: 14-MONITORING yazarı). --- ## Ek: Update Onay Akışı (yönetici gözüyle) ```mermaid flowchart TB TALEP["Update talebi
(güvenlik / MEG / OS)"] --> KAYIT["Değişiklik kaydı
kapsam + geri alma planı"] KAYIT --> ONAY{"Onay yetkilisi
(asıl veya vekil)"} ONAY -->|Ret| KAPANIR["Kapanış kaydı (ret)"] ONAY -->|Onay| LAB["LAB(2) → P1(5) → P2(20)"] LAB --> KAPI{"Sağlık kapısı temiz mi?"} KAPI -->|Halt| DUR["Yayılım DURUR → 17-L3 rollback"] KAPI -->|Temiz| WAVE["W1(50) → W2(100) → PROD(~523)"] WAVE --> RAPOR["Kapanış kaydı
(başarı/arıza/süre)"] ```
# 22 — Dokümantasyon Platformu (Documentation Platform) > Kısa özet: Teknik dokümanların yazıldığı, sürümlendiği ve yayınlandığı platform kararı: Docusaurus önerilir. `docs/*.md` tek kaynaktır; site derlemesi CI'da yapılır; AI ajanları Markdown'u doğrudan günceller. - Dosya: `docs/22-DOCUMENTATION-PLATFORM.md` - İlgili kararlar: `24-DECISION-LOG.md#K-08` (Docusaurus, MkDocs yedekte) - Durum: [ ] Taslak --- ## 1. Amaç 27 dokümanın (00…25 + şablon) hem ham Markdown olarak Git'te hem de aranabilir site olarak yayınlanmasını sağlamak; "doküman nerede, güncel mi, kim değiştirdi" sorularını kapatmak. ## 2. Kapsam - Kapsam içi: platform seçimi, `docs/` ↔ site ilişkisi, yazım-derleme-yayın akışı, sürümleme ve dil politikası. - Kapsam dışı: doküman içerikleri (00…21, 23…25), CI komut detayı (25-ROADMAP'a havale). ## 3. Kararlar ```text KARAR: Dokümantasyon platformu Docusaurus'tur; kaynak `docs-site/` iskeleti + `docs/*.md` tek-kaynak; site derlemesi CI'da yapılır. GEREKÇE: "AI-güncelleme" kriterini yalnızca Git-yerleşik Markdown çözer: ajan `.md`'yi doğrudan yazar, değişiklik PR'dan geçer, geçmiş Git'tedir. Docusaurus yerleşik sürümleme + yerleşik i18n + MDX (Mermaid dahil) verir; derleme maliyeti alternatifleriyle eşdeğerdir. ALTERNATİF: MkDocs + Material — eşdeğer sadelikte güçlü ikinci; Docusaurus hattı sorun çıkarırsa geçiş maliyeti düşük (kaynak yine düz Markdown) → yedekte tutulur. Wiki.js (DB+servis yükü) ve TriliumNext (tek kullanıcı) elendi. RİSK: Node derleme hattı bozulabilir; azaltma: kilitli bağımlılıklar + CI derleme testi; derleme bozulsa bile `docs/*.md` ham haliyle okunur. MALİYET: Ücretsiz (self-hosted/statik yayın). LİSANS: Docusaurus MIT — https://github.com/facebook/docusaurus + https://docusaurus.io/docs/markdown-features/diagrams (doğrulanma: 2026-09-15). ``` ```text KARAR: Yayınlanan sitenin kaynağı ile Git'teki `docs/*.md` arasında elle kopyalama yapılmaz; senkronizasyon tek yönlüdür (Git → site). GEREKÇE: Çift-kaynak, güncel-olmayan site üretir; tek kaynak + CI derlemesi "sitedeki her satırın Git karşılığı var" garantisi verir. ALTERNATİF: Wiki'de düzenle, Git'e aktar — yön tersine döner, PR disiplini ölür; elendi. RİSK: CI bozulursa site eskir; azaltma: CI rozeti + derleme testi her PR'da koşar. MALİYET: Ücretsiz. LİSANS: Yok. ``` ## 4. Neden Bu Karar? Üç filtre (24 §4) burada da uygulandı: (1) ücretsiz — Docusaurus MIT, barındırma statik; (2) sadelik — DB'li wiki (Wiki.js) bir servis daha demektir, reddedildi; tek-kullanıcı bilgi tabanı (TriliumNext) ekip akışını taşımaz; (3) AI-uyumluluk — ajan düz Markdown'u yazar ve review'a sunar; bu, DB-içi editörde mümkün değildir. MkDocs'a karşı Docusaurus'u ayıran, sonradan eklenti aramadan gelen sürümleme + i18n'dir (700 cihazlık filonun dokümanı yıllarca yaşar, sürüm gerekir). ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | MkDocs + Material | Sade, hızlı, güçlü ikinci | Sürümleme/i18n eklentiyle | Yedek | | Wiki.js | Güçlü editör, yetkilendirme | DB+servis yükü, Git akışı dolaylı | Elendi | | TriliumNext | Hafif, kişisel | Ekip review akışı yok | Elendi | | Yalnızca GitHub/GitLab render | Sıfır kurulum | Sürümleme/arama/zengin tema yok | Elendi (ham okuma zaten var) | ## 6. Avantajlar - Derleme bozulsa bile dokümanlar kaybolmaz (`docs/*.md` her zaman okunur). - Mermaid diyagramları (01'deki 3 akış) sitede ve PR önizlemede render olur. - Sürümleme sayesinde "PİLOT'taki prosedür neydi" sorusu cevaplanabilir. ## 7. Dezavantajlar - Node derleme hattı bakım ister (kilit dosyası + periyodik güncelleme). - Editör eşiği wiki'ye göre yüksektir (PR açmak gerekir) — teknik ekip için kabul edilebilir. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Derleme hattı bozulur, site eskir | Orta | Düşük (kaynak okunur) | CI derleme testi + kilitli bağımlılık | | Doküman-Git senkronu bozulur (elle site editi) | Düşük | Orta | Siteye yazma yetkisi yalnızca CI'da | | İçerik eskir (prosedür değişti, doküman kalmış) | Orta | Yüksek | Her PR'da "ilgili doküman güncellendi mi" kontrolü | ## 9. Uygulama Planı 1. `docs-site/` iskeleti kurulur (Docusaurus init), `docs/*.md` siteye kaynak olarak bağlanır. 2. CI job'ı: her PR'da derleme testi (`build`); `main`'e merge'de statik yayın. 3. Yazım kuralı: yeni/degisen prosedür → önce `.md`, sonra PR; site editi yasak. 4. Sürümleme: ilk PROD dalgasında `v1` kesilir; sonraki imaj değişiklikleri yeni sürümle etiketlenir. ```bash # örnek: yerelde önizleme (komut adları İngilizce orijinal, açıklama Türkçe) cd docs-site npm install npm run build npm run serve ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | CI derleme testi (bozuk link/Mermaid) | Hatalı PR kırmızıya düşer | CI | | `docs/*.md` ham okunabilirlik | Git arayüzünde tablolar/diyagramlar anlaşılır | Git web | | Sürüm kesme (`v1`) | Eski prosedüre sürüm menüsünden erişilir | Yayınlanan site | ## 11. Rollback 1. Bozuk site yayını: önceki CI çıktısına (statik) dönülür; kaynak `.md` etkilenmez. 2. Platform değişikliği (Docusaurus → MkDocs): kaynak `.md` taşındığı için içerik kaybı olmaz; yalnızca `docs-site/` yeniden yazılır. ## 12. Kontrol Listesi - [ ] `docs-site/` derleniyor, 3 Mermaid (01) render oluyor. - [ ] CI'da derleme testi her PR'da koşuyor. - [ ] Siteye elle yazma kapalı (yalnızca CI yayınlıyor). - [ ] Şablon (`_TEMPLATE.md`) ile yeni doküman açma akışı denendi. ## 13. Açık Sorular - [ ] Statik sitenin barındırılacağı yer (merkez VDS / iç ağ) (sahibi: 25-ROADMAP). - [ ] İlk sürüm (`v1`) kesme tarihi — PROD dalgasına bağlı (sahibi: merkezi admin, 21). - [ ] Türkçe dışına çeviri ihtiyacı (i18n açılacak mı) (sahibi: yönetim). --- ## Ek: Mermaid — doküman akışı ```mermaid flowchart LR W["Yazar/ajan
docs/*.md düzenler"] --> PR["PR + review"] PR --> CI["CI: Docusaurus build testi"] CI -->|yeşil| MERGE["main'e merge"] MERGE --> PUB["Statik site yayını"] PUB --> READ["Okuyucu
(teknisyen/admin)"] ```
# 23 — Güvenlik ve Lisans Denetimi (Security & License Audit) > Kısa özet: Mimariye giren her ücretli-aday aracın lisans hükmü + ücretsiz karşılığı tek tabloda; AnyDesk'in neden mimariden çıkarıldığı resmi URL'leriyle. Yeni araç öneren önce bu dosyaya bakar. - Dosya: `docs/23-SECURITY-AND-LICENSE-AUDIT.md` - İlgili kararlar: `24-DECISION-LOG.md#K-04` (AnyDesk HAYIR + RustDesk/MeshCentral), `#K-06` (Semaphore Community), `#K-07` (Netdata elendi), `#K-10` (Watchtower) - Durum: [ ] Taslak --- ## 1. Amaç "Şu aracı da kursak mı?" sorusunun lisans cevabını tek yerde vermek ve 700 cihazda lisanssız ticari kullanımı baştan engellemek. Her satırda resmi kaynak URL'si vardır; kulaktan-dolma lisans bilgisi yasaktır. ## 2. Kapsam - Kapsam içi: ücretli-aday araçların hükmü, seçilen ücretsiz karşılıklar, AnyDesk HAYIR hükmü, periyodik denetim rutini. - Kapsam dışı: teknik kurulum (ilgili dokümanlar), firewall/SSH sertleştirme detayı (11-HARDENING). ## 3. Kararlar ```text KARAR: AnyDesk ücretsiz sürümü Blueforce filosunda KULLANILAMAZ; AnyDesk mimariden çıkarılmıştır. GEREKÇE: AnyDesk ücretsiz sürümü yalnızca kişisel kullanımı kapsar; 700 kurumsal saha cihazı ticari kullanımdır ve lisanssız kullanım sözleşme ihlali olur. Kurumsal kullanım en az ücretli plan gerektirir; ayrıca kapalı kaynak + self-hosted yalnızca en üst planda sunulur. ALTERNATİF: Müşteri sahasında zaten lisanslı AnyDesk varsa opsiyonel installer modülü olarak kalabilir (merkezi mimarinin parçası değil) — bunun dışında alternatif yoktur; birincil/ikincil erişim RustDesk OSS + MeshCentral'dır (K-04). RİSK: Sahada "alışkanlıkla" AnyDesk Free kurulur; azaltma: golden image'da AnyDesk yoktur + periyodik denetimde kurulu yazılım taraması yapılır. MALİYET: Kullanılmadığı için lisans bedeli yoktur (kullanılsaydı: ücretli plan zorunlu). LİSANS: AnyDesk lisans şartı — https://support.anydesk.com/docs/anydesk-licenses.md (güncelleme 2025-10-30; doğrulanma: 2026-09-15) + plan listesi https://anydesk.com/en/pricing (doğrulanma: 2026-09-15). ``` ```text KARAR: Zorunlu mimarideki tüm bileşenler FREE katmandadır; ücretli-aday her araç için ücretsiz karşılık aşağıdaki tabloda sabitlenmiştir. Hiçbir playbook, compose veya imaj ücretli özelliğe bağlanamaz. GEREKÇE: 700 cihazda "küçük" lisans bedeli bile çarpanla büyür; ücretsiz-tabana bağlılık hem maliyeti hem denetim yükünü sıfırlar. Karşılık tablosu, gelecekteki "Pro'ya geçelim" baskısına hazır cevap verir. ALTERNATİF: İhtiyaç oldukça ücretli plana geçme — maliyet takibi + sözleşme yükü + kilitlenme; elendi. RİSK: Ücretsiz sürümün kapsamı değişir (upstream fiyat politikası); azaltma: yıllık lisans gözden geçirme (§9 rutin). MALİYET: Ücretsiz (VDS kira bedeli hariç — altyapı maliyeti, lisans değil). LİSANS: Bileşen lisansları 24-DECISION-LOG Ek A'da (doğrulama: 2026-09-15). ``` ## 4. Neden Bu Karar? Lisans denetimi "hukuk işi" değil, mimari iştir: ücretli bir bileşen mimariye girerse 700 cihazın tamamı o sözleşmeye kilitlenir. Bu dosya, her ücretli adayı daha teklif aşamasında ücretsiz karşılığıyla eşleştirir; AnyDesk maddesi bunun en sert örneğidir (kişisel-kullanım tuzağı). Güvenlik boyutu da aynı tablodadır: kapalı kaynak + dış kimlik (Tailscale/ZeroTier tipi) hem lisans hem veri-egemenliği riski taşır. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | İhtiyaç oldukça lisans satın alma | Esnek | 700 çarpanı, sözleşme takibi, kilitlenme | Elendi | | Karma (bazı cihazlar ücretli) | Hedefli harcama | İki sınıf filo, denetim karmaşası | Elendi | | Yıllık lisanssız "görmezden gelme" | Kısa vadeli kolay | Sözleşme ihlali + denetimde ceza | Yasak | ## 6. Avantajlar - Denetimde (müşteri/üst yönetim) her satırın resmi URL'si hazırdır. - Yeni araç teklifi 5 dakikada elenebilir veya kabul edilebilir (tabloya uyar mı?). ## 7. Dezavantajlar - Upstream fiyat/lisans değişiklikleri takip ister (yıllık rutin). - Bazı ücretsiz sürümler özelliksizdir (örn. Semaphore Community'de OIDC/2FA/Vault yok) — bu eksikler operasyonel disiplinle kapatılır (K-06). ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | Sahaya lisanssız ticari yazılım kurulur | Orta | Yüksek | İmajda yok + periyodik kurulu-yazılım taraması | | Upstream free kapsamı daralır | Düşük | Orta | Yıllık gözden geçirme + yedek alternatifler (§9 tablo) | | AGPL yükümlülüğü ihlali (değişiklik dağıtırsak) | Düşük | Orta | Değişikliksiz self-host politikası (RustDesk/Grafana notu) | ## 9. Uygulama Planı ### Ücretli-aday → ücretsiz-karşılık tablosu (FREE sürümler baz) | Ücretli aday | Neden girmedi | Ücretsiz karşılık (mimaride) | Lisans / kaynak | |---|---|---|---| | AnyDesk (ücretli plan) | Free yalnızca kişisel kullanım; 700 cihaz ticari → ihlal | RustDesk OSS hbbs/hbbr + MeshCentral | AGPL-3.0 / Apache-2.0 — https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ ; https://github.com/Ylianst/MeshCentral ; hüküm https://support.anydesk.com/docs/anydesk-licenses.md | | RustDesk Pro | Gerekmez — OSS sınırsız istemci + self-host verir | RustDesk OSS 1.4.9 / sunucu 1.1.16 | AGPL-3.0 — https://rustdesk.com/pricing/ (doğrulanma: 2026-09-15) | | Semaphore Pro/Enterprise | 700 cihaz > Pro 500 node sınırı; Community yeterli | Semaphore UI Community | MIT, Community $0 — https://semaphoreui.com/pricing + https://github.com/semaphoreui/semaphore | | Netdata Cloud (ücretli) | Free 5 node kotası 700'de ücrete düşer | Prometheus + Node Exporter + Grafana OSS + Uptime Kuma | Apache-2.0 / AGPL-3.0 / MIT (24 Ek A 23–25) | | Tailscale / ZeroTier (ücretli kademeler) | Freemium + merkezi kimlik | WireGuard (self-hosted hub-spoke) | OSS — https://www.wireguard.com/quickstart/ | | Ubuntu Pro / ESM | Mimari ESM'e bel bağlamaz | Ubuntu LTS ücretsiz + onaylı update dalgaları | https://releases.ubuntu.com/ | | Docker Business / Scout | Mimaride yok | Docker CE + sabit tag disiplini | https://docs.docker.com/ | | Wiki.js Cloud / Confluence | DB yükü + ücret | Docusaurus (MIT) | https://github.com/facebook/docusaurus | | AWX (operatör maliyeti) / Salt / Rudder Enterprise | K8s/agent/relay yükü | Ansible CLI + Semaphore Community | GPL-3.0 / MIT — https://github.com/ansible/ansible | ```bash # örnek: sahada lisanssız/izinsiz yazılım taraması (merkezden) ansible all -m shell -a "dpkg -l | grep -i -E 'anydesk|teamviewer' || echo TEMIZ" # örnek: Watchtower kalıntısı taraması (politika ihlali, K-10) ansible all -m shell -a "docker ps --format '{{.Names}} {{.Image}}' | grep -i watchtower || echo TEMIZ" ``` ### Periyodik rutin 1. Üç ayda bir: yukarıdaki taramalar + `latest` etiketi denetimi + `unattended-upgrades` kapalılık kontrolü (K-11). 2. Yılda bir: tablo satırlarındaki resmi URL'ler yeniden çekilir, fiyat/lisans değişikliği varsa bu dosya PR ile güncellenir. 3. Yeni araç teklifi: tabloya satır eklenmeden kuruluma girmez (21'deki onay yetkilisi denetler). ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | İmajda AnyDesk/TeamViewer paketi yok | `dpkg -l` temiz | LAB(2) imaj denetimi | | İmajda Watchtower yok, `latest` yok | `docker images` sabit tag | LAB(2) | | `unattended-upgrades` kapalı | Timer'lar maskeli, değerler `0` | LAB(2) | ## 11. Rollback Lisans değişikliği (upstream free kapsamı daralırsa): yedek alternatife (tablodaki "Yedek" satırları, örn. MkDocs) geçilir; değişiklik 24-DECISION-LOG PR'ı ile yapılır, bu dosya aynı PR'da güncellenir. ## 12. Kontrol Listesi - [ ] AnyDesk HAYIR hükmü resmi URL'li (§3 birinci blok). - [ ] Her ücretli-adayın ücretsiz karşılığı tabloda + URL'li. - [ ] Üç aylık tarama job'ları Semaphore'da tanımlı. - [ ] Yıllık URL tazeleme takvime işlendi. ## 13. Açık Sorular - [ ] Üç aylık tarama sonuçlarının raporlanacağı merci (sahibi: merkezi admin, 21). - [ ] AGPL değişiklik-dağıtım politikasının hukukça gözden geçirilmesi gerekli mi (sahibi: yönetim). - [ ] Müşteri-lisanslı AnyDesk modülünün şartları (sahibi: 07-REMOTE-ACCESS yazarı). --- ## Ek: Lisans Karar Akışı (yeni araç teklifi) ```mermaid flowchart TB TEKLIF["Yeni araç teklifi"] --> UCRET{"Ücretli katman / lisans gerektirir mi?"} UCRET -->|Hayır (OSS, kotasız)| KABUL["Mimariye alınabilir
(karşılık tablosuna işlenir)"] UCRET -->|Evet| KARSILIK{"Ücretsiz karşılığı tabloda var mı?"} KARSILIK -->|Var| RED["Teklif elenir
karşılık kullanılır"] KARSILIK -->|Yok| PR["24-DECISION-LOG PR'ı açılır
Ek A'ya URL + sürüm + tarih"] ```
# 24 — Karar Günlüğü (Decision Log) > Kısa özet: Blueforce 700 cihaz Linux filosunun tüm nihai mimari kararları tek tabloda; her karar KARAR/GEREKÇE/ALTERNATİF/RİSK/MALİYET/LİSANS bloğu + resmi kaynak URL ile. Diğer 25 dokümanın başvurduğu kilit dosyadır. > > - Dosya: `docs/24-DECISION-LOG.md` > - İlgili kararlar: bu dosyanın kendisi kilit kaynaktır; `01-ARCHITECTURE.md` bu kararları diyagramlaştırır. > - Durum: [x] Onaylı (Faz 2 kilidi; değişiklik bu dosyadan PR ile yapılır) > - Kural: tüm kararlarda FREE sürümler baz alınır; Pro/Enterprise zorunlu mimariye girmez. > - Doğrulama günü: 2026-09-15 (tüm URL'ler bu tarihte HTTP 200 ile çekildi; ayrıntı `research/` dosyalarında). --- ## 1. Amaç 27 izlenebilirlik sorusunun (bkz. §13 ve Ek B) her birinin hangi karara dayandığını tek yerden göstermek; kararsız veya varsayıma dayalı seçim bırakmamak. ## 2. Kapsam - Kapsam içi: OS/GUI/boot, 4 erişim kanalı, WireGuard, fleet, monitoring, docs, golden image, Docker, cihaz kimliği, update dalgaları — aşağıdaki K-01…K-14 kararları. - Kapsam dışı: script kodları, VDS kurulum komutları, gerçek cihaz test sonuçları (Faz 3+ işi; ilgili dokümanlara havale edilir). ## 3. Kararlar ### K-01 — Baz işletim sistemi ve sürüm ```text KARAR: Saha standardı Ubuntu Server 26.04.1+ LTS'tir (GUI'siz minimal kurulum; GNOME sonradan eklenir). GEREKÇE: 26.04.1 LTS resmi indirme sayfasında yayında (kod adı Resolute Raccoon); Server ISO'su varsayılan olarak GUI kurmaz, minimal zemin + ihtiyaca göre GUI standart yoldur. Noktasürüm (.1+) baz alınır çünkü ilk .0 dalgasındaki kurulumcu hataları genelde .1'de toplanır (LAB imaj testiyle kesinleşir). ALTERNATİF: Ubuntu 24.04 LTS — 26.04 doğrulanamasaydı yedekti; doğrulandığı için yedekte bekler. Ubuntu Desktop 26.04 (6 GB RAM şartı) saha bütçesini zorladığı için elendi. RİSK: 26.04.x noktasürümlerinde donanım sürücü farkı; azaltma: golden image LAB(2)'de her saha donanım profilinde test edilir. MALİYET: Ücretsiz. LİSANS: Ubuntu LTS ücretsiz (açık kaynak bileşenler; ESM kapsamına bel bağlanmaz) — https://releases.ubuntu.com/ (26.04.1, doğrulanma: 2026-09-15) + https://ubuntu.com/download/desktop (sistem gereksinimi, doğrulanma: 2026-09-15). ``` ### K-02 — Masaüstü ortamı (GNOME) ```text KARAR: Saha masaüstü ortamı GNOME'dur (Ubuntu Desktop standardı); XFCE elendi. GEREKÇE: Kullanıcı kararı + Ubuntu Desktop standardı: tanıdık modern masaüstü, teknisyen akışıyla birebir uyum. ALTERNATİF: XFCE — hafif ve düşük kaynak tüketimine rağmen kullanıcı kararıyla elendi; yedekte tutulmaz. RİSK: xRDP+GNOME kombinasyonu ek ayar isteyebilir (uyarı: GNOME oturumu RDP'de ek yapılandırma gerektirebilir); azaltma: 13-MEG-LINUX-ACCEPTANCE testi GNOME üzerinde koşar, `bf-gui-*` LAB'da 26.04.1 imajında doğrulanmadan dondurulmaz. MALİYET: Ücretsiz. LİSANS: GNOME (GPL/LGPL bileşenler), ücretsiz — https://www.gnome.org/ (doğrulanma: 2026-09-15). ``` ### K-03 — Boot davranışı ve GUI aç/kapa ```text KARAR: Cihazlar terminale (multi-user) boot eder; grafik katman `bf-gui-on` / `bf-gui-off` ile systemd üzerinden açılıp kapatılır. GEREKÇE: xRDP mimarisi oturum yöneticisi (sesman) + systemd servis birimleri (`xrdp`, `xrdp-sesman`) üzerine kuruludur; GUI, display-manager + xRDP servisleri üzerinden yönetilebilir bir katman olarak modellenir. Varsayılan kapalı GUI = düşük RAM, düşük saldırı yüzeyi, öngörülebilir boot. ALTERNATİF: Her zaman grafik boot — kaynak israfı ve 700 cihazda gereksiz hata yüzeyi nedeniyle elendi. RİSK: Kesin birim adları ve GNOME oturum başlatma satırı 26.04.1'e göre değişebilir; azaltma: LAB'da 26.04.1 imajında doğrulanmadan `bf-gui-*` dondurulmaz. MALİYET: Ücretsiz. LİSANS: xRDP Apache-2.0, ücretsiz — https://github.com/neutrinolabs/xrdp (v0.10.6.1, 2026-07-07; doğrulanma: 2026-09-15). ``` ### K-04 — Uzaktan erişim kanalları (4 kanal) ```text KARAR: Dört erişim kanalı: (1) SSH-over-WireGuard (yönetim/otomasyon), (2) xRDP + GNOME (grafik bakım), (3) RustDesk OSS self-hosted hbbs/hbbr (birincil grafik uzak erişim, WireGuard-bağımsız), (4) MeshCentral self-hosted (ikincil yönetim: terminal/dosya/envanter, WireGuard-bağımsız). AnyDesk mimariden ÇIKARILDI. GEREKÇE: RustDesk OSS sınırsız istemci, kendi ID+röle altyapısı, E2E şifreli oturum, dosya transferi ve katılımsız erişimi ücretsiz verir; istemcide VDS genel adresi kullanıldığında WireGuard çökse bile kanal ayakta kalır. MeshCentral Apache-2.0 ile ikinci bağımsız düzlemi sağlar. AnyDesk ücretsiz sürümü yalnızca kişisel kullanımı kapsar; 700 kurumsal cihaz ticari kullanımdır ve lisanssız kullanım ihlal olur (en az Advanced planı gerekirdi). ALTERNATİF: AnyDesk (lisans maliyeti + kapalı kaynak + self-hosted yalnızca en üst planda → elendi; yalnızca müşteri sahasında zaten lisanslıysa opsiyonel installer modülü). Tailscale/ZeroTier (freemium, merkezi kimlik → elendi). RİSK: İki VDS-bağımlı kanalın tek ortak noktası VDS + genel internet; azaltma: hbbs/hbbr `--net=host` + health monitörü (Uptime Kuma) + yedek VDS planı 17-RECOVERY'de. MALİYET: Ücretsiz (kendi VDS'i üzerinde; VDS kira bedeli hariç — altyapı maliyeti, lisans değil). LİSANS: RustDesk istemci+sunucu AGPL-3.0 (1.4.9 / 1.1.16) — https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ + https://rustdesk.com/docs/en/self-host/client-deployment/ (doğrulanma: 2026-09-15); MeshCentral Apache-2.0 (1.2.5) — https://github.com/Ylianst/MeshCentral (doğrulanma: 2026-09-15); AnyDesk hükmü — https://support.anydesk.com/docs/anydesk-licenses.md (doğrulanma: 2026-09-15) + https://anydesk.com/en/pricing. ``` ### K-05 — WireGuard modeli ```text KARAR: Hub-spoke WireGuard: merkez sunucu hub, her saha cihazı spoke (peer adı = `bf-<8hane>`); istemcide `PersistentKeepalive = 25` + `wg-quick@wg0` systemd enable; RDP/SSH dış dünyaya kapalı, yalnızca WireGuard arkasında. GEREKÇE: Resmi QuickStart NAT arkasındaki eş için 25 sn keepalive'i "geniş güvenlik-duvarı yelpazesiyle çalışan makul aralık" olarak verir; `wg-quick@.service` Ubuntu paketinden gelir, enable = açılışta otomatik VPN. Elektrik kesintisi sonrası cihaz açılınca VPN kendiliğinden gelir. ALTERNATİF: Keepalive kapalı / agresif 10 sn — kapalı NAT eşlemesini düşürür, agresif değer saha verisi olmadan yazılmaz; PİLOT-1 kopma sayacına göre ayarlanır. RİSK: Turkcell CGNAT davranışı resmi kaynaktan doğrulanamaz (operatör bilgisi); azaltma: PİLOT-1'de 7/24 kopma sayacı, aralık saha verisiyle ayarlanır. MALİYET: Ücretsiz. LİSANS: WireGuard OSS (Ubuntu paketi `wireguard-tools 1.0.20250521-1ubuntu1`) — https://www.wireguard.com/quickstart/ + https://packages.ubuntu.com/resolute/wireguard-tools (doğrulanma: 2026-09-15). ``` ### K-06 — Filo yönetimi ```text KARAR: Birincil filo otomasyonu Ansible CLI (SSH, agent'sız); operatör arayüzü Semaphore UI Community (self-hosted, MIT). Hiçbir playbook Pro/Enterprise özelliğine bağlanmaz. GEREKÇE: En düşük hareketli parça (SSH + YAML + tek Go binary); 700 cihazda ek agent/master/K8s maliyeti yok. Community ücretsizliği resmi fiyat sayfasıyla kanıtlı ("$0, free forever"); Pro en fazla 500 managed node desteklediği için 700 cihazda ücretli yol zaten kapalı. ALTERNATİF: AWX (K8s yükü + release'ler refactoring nedeniyle duraklatıldı → elendi); Salt (minion agent + master PKI → elendi); Rudder Community (relay/Enterprise modülleri + öğrenme eğrisi → elendi); MeshCentral (erişim aracı, filo otomasyonu değil → bu rolde elendi). RİSK: Semaphore tek nokta arızası → Ansible CLI bağımsız çalışır (UI çökse filo durmaz). Community'de OIDC/2FA/Vault yok → Semaphore yalnızca WireGuard arkasında, erişim VPN + SSH anahtarı ile; gizliler yerleşik şifreli Key Store'da. MALİYET: Ücretsiz (kendi VDS'i üzerinde). LİSANS: Ansible GPL-3.0 — https://github.com/ansible/ansible; Semaphore Community MIT — https://github.com/semaphoreui/semaphore + https://semaphoreui.com/pricing (doğrulanma: 2026-09-15). ``` ### K-07 — Monitoring ```text KARAR: Birincil Prometheus + Node Exporter + Grafana OSS (17 metriğin tamamı); tamamlayıcı Uptime Kuma (saha-teknisyeni dostu UP/DOWN panosu + Push "last seen" + bildirimler). Netdata elendi. GEREKÇE: 17 metriğin tamamı yalnızca Prometheus yığınında ücretsiz ve kotasız toplanır (WireGuard handshake, Docker, MEG sağlığı textfile collector ile). Netdata Cloud Free 5 node kotası 700 cihazda ücrete düşer. Uptime Kuma metrik vermez ama en basit çevrimdışı alarmını verir (20 sn aralık, 90+ bildirim servisi, tek konteyner). ALTERNATİF: Yalnızca Uptime Kuma (metrik körlüğü → elendi); Netdata self-hosted Parent (merkezi tasarım yükü + kota riski → elendi). RİSK: 700 node'da saklama/kardinalite; azaltma: scrape 60 sn, retention 30–90 gün, etiket şeması `BF-` standardında, federasyon/fazlı rollout. MALİYET: Ücretsiz. LİSANS: Prometheus/Node Exporter Apache-2.0 — https://github.com/prometheus/prometheus + https://prometheus.io/docs/guides/node-exporter/; Grafana OSS AGPL-3.0 (değişikliksiz self-host) — https://grafana.com/oss/; Uptime Kuma MIT — https://github.com/louislam/uptime-kuma (doğrulanma: 2026-09-15). ``` ### K-08 — Doküman platformu ```text KARAR: Docusaurus (`docs-site/` iskeleti; `docs/*.md` tek kaynak, site derlemesi CI'da). GEREKÇE: "AI-güncelleme" kriterini yalnızca Git-yerleşik Markdown çözer (ajan `.md`'yi doğrudan yazar, review PR'dan geçer). Wiki.js'nin DB+servis yükü sadelik kuralını bozar; TriliumNext tek-kullanıcı bilgi tabanıdır. MkDocs'a karşı Docusaurus: yerleşik sürümleme + yerleşik i18n + MDX; derleme maliyeti eşdeğerdir. ALTERNATİF: MkDocs + Material (eşdeğer sadelikte güçlü ikinci; Docusaurus derleme hattı sorun çıkarırsa geçiş maliyeti düşük çünkü kaynak yine düz Markdown — yedekte tutulur). Wiki.js (DB yükü → elendi). TriliumNext (ekip akışı yok → elendi). RİSK: Node derleme hattı bozulabilir; azaltma: kilitli bağımlılıklar + CI derleme testi; derleme bozulsa bile `docs/*.md` ham haliyle okunur. MALİYET: Ücretsiz. LİSANS: Docusaurus MIT — https://github.com/facebook/docusaurus + https://docusaurus.io/docs/markdown-features/diagrams (doğrulanma: 2026-09-15). ``` ### K-09 — Golden image yöntemi ```text KARAR: Golden image = Ubuntu Server 26.04.1+ minimal + `unattended-upgrades` kapalı + `blueforce-install.sh` tek kurucu (bayi no → READY); imaj, preseed/autoinstall dosyasıyla üretilir, cihaz-spesifik kimlik (`bf-`) imajda DEĞİL ilk açılışta verilir. GEREKÇE: Donanım heterojenliği bilinmediği için bit-kopya klon (Clonezilla) tek imajda kırılgandır; autoinstall + idempotent kurulum scripti her donanım profilinde aynı zemini kurar, kimlik ilk boot'ta enjekte edilir. Hedef akış: USB → Ubuntu → tek script → bayi no → READY. ALTERNATİF: Clonezilla bit-kopya (hızlı ama donanım-fragil → yedek yöntem olarak tutulur); Packer/custom ISO (ek derleme hattı yükü → elendi); cloud-init (autoinstall'a göre saha-USB akışına daha az uygun → elendi). RİSK: İlk imaj heterojen donanımda sürücü eksiltebilir; azaltma: önce `bf-hardware-inventory.sh` taraması (18-ROLLOUT), LAB'da her profilde test. MALİYET: Ücretsiz. LİSANS: Ubuntu autoinstall/subiquity (ücretsiz, Ubuntu lisans seti içinde) — https://releases.ubuntu.com/26.04/ (doğrulanma: 2026-09-15). NOT: Faz 1'de resmi autoinstall sözdizimi ayrıca doğrulanmadı; kesin direktifler 04-GOLDEN-IMAGE'da LAB çıktısıyla yazılır. ``` ### K-10 — Docker restart politikası ve imaj disiplini ```text KARAR: Tüm saha konteynerlerinde restart politikası `unless-stopped`; `latest` etiketi yasak (sabit tag zorunlu); Watchtower yok; log rotasyonu (`max-size`/`max-file`) daemon.json'da zorunlu; yayımlanan portlar `127.0.0.1`'e bağlanır (UFW bypass'a karşı). GEREKÇE: `unless-stopped`, `always`'in aksine teknisyenin bakım için durdurduğu konteyneri reboot sonrası sürpriz kalkıştan korur; elektrik kesintisi kurtarması aynen çalışır (kesinti "manuel stop" sayılmaz). Hareketli `latest` etiketi 700 cihazda determinizmi bozar ("onaysız update yasak" politikası). Watchtower hem politikayı ihlal eder (otomatik updater) hem upstream'de arşivlenmiştir. Docker varsayılan `json-file` logu rotasyon yapmaz → dolan disk = kilitlenen cihaz. Docker yayımlanan portlarda UFW'yi baypas eder (resmi belgeli) → localhost-bind + WireGuard üzerinden erişim. ALTERNATİF: `always` (öngörülemez bakım davranışı → elendi); `iptables:false` (resmi doküman "çoğu kullanıcı için uygun değil" diye uyarır → varsayılan çözüm değil); digest pinleme (`@sha256`) PİLOT-2'de değerlendirilecek ek sıkılık adımı. RİSK: `max-size/max-file` değerleri MEG log hacmine göre ayarlanmazsa ya disk dolar ya log kaybolur; azaltma: PİLOT'ta hacim ölçülür, 12-DOCKER'da sabitlenir. MALİYET: Ücretsiz (Docker CE). LİSANS: Docker CE ücretsiz (Business/Scout mimaride yok) — https://docs.docker.com/engine/containers/start-containers-automatically/ + https://docs.docker.com/engine/logging/configure/ + https://docs.docker.com/engine/network/packet-filtering-firewalls/ (doğrulanma: 2026-09-15); Watchtower arşiv durumu — https://github.com/containrrr/watchtower (doğrulanma: 2026-09-15). ``` ### K-11 — Otomatik güncelleme yasağı ```text KARAR: Golden image'da `unattended-upgrades` kapatılır (`/etc/apt/apt.conf.d/20auto-upgrades` değerleri `0` + `apt-daily*.timer` maskeleme); tüm güncellemeler LAB→PİLOT→WAVE→PROD onay zincirinden geçer (dalgalar: LAB(2)/P1(5)/P2(20)/W1(50)/W2(100)/PROD). GEREKÇE: `unattended-upgrades` Ubuntu Server'da varsayılan kurulu ve etkindir, günde bir kez çalışır; kapatılmazsa 700 cihaz ilk açılışta güncellemeye kalkar ("onaysız update yasak" ilkesi delinir). Kaçan timer makine açılışında hemen tetiklenir (`Persistent=true` davranışı) — saha PC'leri sık kapanan makinelerdir. ALTERNATİF: Otomatik security update'e izin verme — 700 cihazda eşzamanlı indirme + onaysız değişim riski nedeniyle elendi; security update'ler öncelik işaretli onaylı dalga ile çıkar. RİSK: Kapatma adımı imajda atlanırsa filo kendiliğinden güncellenir; azaltma: `blueforce-install.sh` final-check + monitoring'de `Unattended-Upgrade` durumu metriği. MALİYET: Ücretsiz. LİSANS: Yok (işletim sistemi yapılandırması) — https://ubuntu.com/server/docs/how-to/software/automatic-updates/ + https://manpages.ubuntu.com/manpages/resolute/en/man8/unattended-upgrade.8.html (doğrulanma: 2026-09-15). ``` ### K-12 — Cihaz kimliği standardı ```text KARAR: 8 haneli bayi no → Device ID `BF-` (büyük), hostname `bf-` (küçük); WireGuard peer adı, RustDesk etiketi, Ansible inventory adı, monitoring etiketi hepsi aynı ID'yi kullanır. Örnek: `BF-12010193` / `bf-12010193`. GEREKÇE: Tek kimlik = 9 sistemde (DNS/hostname, WireGuard, RustDesk, MeshCentral, Ansible, Prometheus, log, envanter, runbook) join anahtarı; büyük/küçük ayrımı insan-makine karışıklığını önler (insan `BF-`, makine `bf-`). ALTERNATİF: Rastgele UUID / MAC-tabanlı ad — saha teknisyeni için okunamaz, bayi eşleşmesi manuel olur → elendi. RİSK: Bayi no değişirse kimlik çakışması; azaltma: 02-NAMING'de yeniden-no prosedürü + envanter benzersizlik kontrolü. MALİYET: Ücretsiz. LİSANS: Yok (isimlendirme standardı). ``` ### K-13 — SSH kimlik doğrulama mimarisi ```text KARAR: `blueforce` bakım kullanıcısı + anahtar zorunlu (password SSH kapalı, root SSH kapalı); 700 cihaza tek private key YAYILMAZ — admin başına anahtar + cihazlarda `authorized_keys` merkezi dağıtımı (Ansible), uzun vadede SSH CA hedefi. GEREKÇE: Tek private key'in sızıntı blast-radius'u 700 cihazdır; per-admin anahtar + merkezi dağıtım sızıntıyı tek anahtarın iptaline indirger. Servisler ayrı system user ile çalışır, sudo kontrollüdür. ALTERNATİF: Tek paylaşılan private key (basit ama 700 cihazlık blast-radius → elendi); password auth (kaba-kuvvet yüzeyi → elendi). RİSK: Anahtar dağıtım gecikmesi yeni cihazda erişimsizlik; azaltma: golden image'da bootstrap anahtarı + ilk Ansible run'ında rotasyon. MALİYET: Ücretsiz. LİSANS: OpenSSH (BSD-lisanslı), ücretsiz. ``` ### K-14 — Log ve sağlık disiplini ```text KARAR: Birincil metrik/monitoring K-07; log tarafında journald + Docker `json-file` rotasyonlu + textfile `.prom` (WireGuard handshake, Docker, MEG) + `bf-status` / `bf-diagnostics` / `bf-support-bundle` (secret sızdırmaz) sözleşmeleri. GEREKÇE: 700 dokunulmaz cihazda logsuz metrik kör, metriksiz log sağırdır; textfile collector özel metriklerin (WireGuard/MEG) standart Prometheus yoludur. Support-bundle secret dışlama kuralıyla güvenli uzaktan tanı sağlar. ALTERNATİF: Harici log platformu (Loki/ELK) ilk fazda — ek servis yükü, sadelik kuralını bozar → Faz-sonrası hedef olarak 15-LOGGING'e not edilir. RİSK: Log hacmi retention'ı şişirir; azaltma: PİLOT'ta hacim ölçümü + limitler 15-LOGGING'de. MALİYET: Ücretsiz. LİSANS: systemd-journald / Node Exporter textfile (Apache-2.0) — https://github.com/prometheus/node_exporter (doğrulanma: 2026-09-15). ``` --- ## 4. Neden Bu Karar? Tüm kararlarda aynı üç filtre uygulandı: (1) %100 ücretsiz/self-hosted — resmi URL + sürüm + tarih olmadan lisans iddiası yazılmadı; (2) sadelik — Kubernetes, microservice, gereksiz agent/DB/cloud önerisi reddedildi (AWX'in K8s zorunluluğu, Salt master PKI, Wiki.js DB yükü, harici log platformu); (3) onaysız update yasağı — unattended-upgrades, `latest`, Watchtower aynı ilkeden elendi. 700 ölçeğinde her "küçük varsayılan" (günlük otomatik update, rotasyonsuz log, UFW bypass) filo-çapı arızaya dönüşür; kararlar bu çarpanı küçültür. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Ubuntu 24.04 LTS | Olgun donanım desteği | 26.04 doğrulandı, daha kısa destek penceresi | Yedek | | XFCE | Hafif, düşük kaynak | Kullanıcı kararıyla elendi (saha standardı GNOME) | Elendi | | AnyDesk | Tanınmış, kolay | Ticari lisans zorunlu (700 cihaz), kapalı kaynak | Elendi | | Tailscale/ZeroTier | Kolay mesh | Freemium, merkezi kimlik | Elendi | | AWX | Güçlü UI | K8s zorunlu, release'ler duraklatıldı | Elendi | | Salt / Rudder | Ölçeklenir | Agent+master/relay yükü, öğrenme eğrisi | Elendi | | Netdata Cloud | Zengin agent | Free 5 node kotası, 700 cihaz ücretli | Elendi | | Yalnızca Uptime Kuma | Çok kolay | Metrik körlüğü | Elendi | | Wiki.js | Güçlü editör | DB+servis yükü, Git akışı dolaylı | Elendi | | MkDocs + Material | Eşdeğer sade | Sürümleme/i18n eklentiyle | Yedek (güçlü ikinci) | | TriliumNext | Hafif | Tek kullanıcı, ekip akışı yok | Elendi | | Clonezilla bit-kopya | Hızlı klon | Donanım-fragil | Yedek yöntem | | Docker `always` | Her koşulda ayakta | Bakım-stop'u reboot'ta deler | Elendi | | Watchtower | Otomatik imaj | Politika ihlali + arşivlenmiş upstream | Elendi | | Tek paylaşılan SSH key | Basit | 700 cihazlık blast-radius | Elendi | ## 6. Avantajlar - 14 kararın tamamı ücretsiz katmanda kanıtlı; zorunlu mimaride lisans bedeli yok. - Her kararın resmi kaynak URL'si + sürümü + doğrulama tarihi var; varsayıma dayalı karar yok (belirsizler LAB/PİLOT'a havale edildi). - 4 erişim kanalından ikisi WireGuard-bağımsız; tek tünel arızası filoyu kör etmez. ## 7. Dezavantajlar - K-09 (golden image) Faz 1'de tam resmi-doğrulamalı değil; autoinstall sözdizimi LAB çıktısına bağımlı. - Semaphore Community'de OIDC/2FA/Vault yokluğu operasyonel disiplinle (VPN-arkası + Key Store) kapatılıyor; bu disiplin bozulursa güvenlik açığı doğar. - Prometheus 700 node işletme bilgisi ister; retention/kardinalite LAB'da ölçülmeden donanım siparişi verilmemeli. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | `unattended-upgrades` imajda açık unutulur | Orta | Yüksek (filo kendiliğinden güncellenir) | install.sh final-check + monitoring metriği (K-11) | | Docker UFW bypass ile MEG portu dışa açılır | Orta | Yüksek | localhost-bind standardı + 11-HARDENING denetimi (K-10) | | Semaphore çöker, filo yönetimsiz kalır | Düşük | Orta | Ansible CLI bağımsız çalışır (K-06) | | 26.04.x sürücü farkı heterojen donanımda | Orta | Orta | LAB'da profil başına imaj testi (K-01/K-09) | | Tek VDS iki bağımsız kanalı da barındırırsa ortak arıza | Düşük | Yüksek | hbbs/hbbr + MeshCentral ayrı VDS veya yedek planı (K-04) | | 700 node Prometheus kardinalite patlaması | Orta | Orta | 60 sn scrape, 30–90 gün retention, `BF-` etiket şeması (K-07) | ## 9. Uygulama Planı 1. Golden image'a K-01/K-02/K-03/K-10/K-11 uygulanır (04-GOLDEN-IMAGE). 2. WireGuard hub + ilk peer'lar K-05'e göre kurulur (08-WIREGUARD). 3. hbbs/hbbr + MeshCentral VDS'leri K-04'e göre ayağa kaldırılır (07-REMOTE-ACCESS). 4. Ansible + Semaphore K-06'ya göre kurulur; ilk playbook LAB(2)'de koşar (09-FLEET). 5. Prometheus + Uptime Kuma K-07'ye göre kurulur; 17 metrik LAB'da doğrulanır (14-MONITORING). 6. Update zinciri K-11 dalgalarıyla işletilir (10-UPDATE). ```bash # örnek: cihaz kimliği standardı (K-12) hostnamectl set-hostname bf-12010193 ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | `20auto-upgrades` değerleri `0`, timer'lar maskeli | Otomatik apt çalışmaz | LAB(2) imaj denetimi | | `bf-gui-on/off` sonrası RDP oturumu | GNOME oturumu açılır/kapanır | LAB(2) | | WireGuard down iken RustDesk oturumu | Bağlantı kurulur (bağımsız kanal) | P1(5) | | `docker inspect` restart politikası | `unless-stopped` tüm saha konteynerlerinde | LAB(2) | | Semaphore kapalı iken Ansible CLI run | Playbook başarıyla koşar | LAB(2) | | Scrape + 17 metrik + Uptime Push | Grafana + Kuma panoları dolu | P1(5) | ## 11. Rollback 1. Yanlış karar tespitinde bu dosya PR ile düzeltilir; bağımlı dokümanlar (01, 03, 07, 08, 09, 12, 14) aynı PR'da güncellenir. 2. İmaj-kritik karar (K-01/K-11) geri alınırsa golden image yeniden üretilir, sahaya sürülmüş dalga 10-UPDATE rollback zinciriyle geri alınır. 3. Son çare: karar öncesi imaj sürümüne dönüş (17-RECOVERY LEVEL 9). ## 12. Kontrol Listesi - [ ] 14 kararın her birinde KARAR/GEREKÇE/ALTERNATİF/RİSK/MALİYET/LİSANS + kaynak URL var. - [ ] Ücretli hiçbir araç zorunlu mimaride değil; AnyDesk hükmü resmi URL'li. - [ ] `unattended-upgrades`, `latest`, Watchtower yasakları üç dosyada da tutarlı (03/10/12). - [ ] Ek B'deki 27 sorunun her biri ≥1 karara bağlı (boş hücre yok). ## 13. Açık Sorular - [ ] GNOME oturum başlatma satırı + `bf-gui-*` kesin birim listesi — LAB 26.04.1 imajında (sahibi: Faz 3, 03/04 yazarları). - [ ] MEG log hacmine göre `max-size/max-file` kesin değerleri — PİLOT ölçümü (sahibi: 12-DOCKER yazarı). - [ ] Turkcell NAT kopma sayacı + keepalive ayarı — P1 7/24 sayaç (sahibi: 08-WIREGUARD yazarı). - [ ] Digest pinleme (`@sha256`) kararı — PİLOT-2 değerlendirmesi (sahibi: 12-DOCKER yazarı). - [ ] Prometheus retention/kardinalite hesabı (700 × 60 sn) — LAB ölçümü (sahibi: 14-MONITORING yazarı). - [ ] Semaphore Global Runner sayısı + eşzamanlılık — 700 node yük testi (sahibi: 09-FLEET yazarı). --- ## Ek A: Kaynak Tablosu (URL + sürüm + tarih) | # | Kaynak | Sürüm/tarih | |---|---|---| | 1 | https://releases.ubuntu.com/ | 26.04.1 LTS Resolute Raccoon, 2026-09-15 | | 2 | https://releases.ubuntu.com/26.04/ | Server GUI kurmaz notu, 2026-09-15 | | 3 | https://ubuntu.com/download/desktop | 2 GHz / 6 GB / 25 GB, 2026-09-15 | | 4 | https://ubuntu.com/server/docs/how-to/software/automatic-updates/ | güncel Server docs, 2026-09-15 | | 5 | https://manpages.ubuntu.com/manpages/resolute/en/man8/unattended-upgrade.8.html | 2.12ubuntu9, 2026-09-15 | | 6 | https://www.gnome.org/ | Desktop standardı beyanı, 2026-09-15 | | 7 | https://github.com/neutrinolabs/xrdp | v0.10.6.1 (2026-07-07), 2026-09-15 | | 8 | https://www.wireguard.com/quickstart/ | keepalive 25 sn, 2026-09-15 | | 9 | https://packages.ubuntu.com/resolute/wireguard-tools | 1.0.20250521-1ubuntu1, 2026-09-15 | | 10 | https://docs.docker.com/engine/network/packet-filtering-firewalls/ | UFW bypass, 2026-09-15 | | 11 | https://docs.docker.com/engine/logging/configure/ | log rotasyon, 2026-09-15 | | 12 | https://docs.docker.com/engine/containers/start-containers-automatically/ | unless-stopped, 2026-09-15 | | 13 | https://github.com/containrrr/watchtower | arşivli, 2026-09-15 | | 14 | https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ | mimari hbbs/hbbr, 2026-09-15 | | 15 | https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/docker/ | compose + portlar, 2026-09-15 | | 16 | https://rustdesk.com/docs/en/self-host/client-deployment/ | OSS toplu dağıtım, 2026-09-15 | | 17 | https://rustdesk.com/pricing/ | Individual ~11,88 / Basic ~23,88 USD/ay, 2026-09-15 | | 18 | https://support.anydesk.com/docs/anydesk-licenses.md | ticari lisans şartı (güncelleme 2025-10-30), 2026-09-15 | | 19 | https://anydesk.com/en/pricing | Solo/Standard/Advanced/Ultimate, 2026-09-15 | | 20 | https://github.com/Ylianst/MeshCentral | 1.2.5 (2026-08-12), Apache-2.0, 2026-09-15 | | 21 | https://github.com/ansible/ansible | GPL-3.0, 2026-09-15 | | 22 | https://github.com/semaphoreui/semaphore + https://semaphoreui.com/pricing | MIT, Community $0, 2026-09-15 | | 23 | https://github.com/prometheus/prometheus + https://prometheus.io/docs/guides/node-exporter/ | Apache-2.0, 2026-09-15 | | 24 | https://grafana.com/oss/ | AGPL-3.0 OSS, 2026-09-15 | | 25 | https://github.com/louislam/uptime-kuma | MIT, 2026-09-15 | | 26 | https://github.com/facebook/docusaurus + https://docusaurus.io/docs/markdown-features/diagrams | MIT, 2026-09-15 | | 27 | https://github.com/prometheus/node_exporter | textfile collector, 2026-09-15 | --- ## Ek B: 27 Soru → Karar İzlenebilirliği > NOT: `SUMMARY.md` Faz 4'te üretilecek; aşağıdaki S-01…S-27 listesi şartname + plandaki 27 kritik sorunun Faz 2 karşılığıdır. SUMMARY yazıldığında her cevap buradaki karara linklenecek. | # | Soru | Karar | Doküman | |---|---|---|---| | S-01 | Hangi Ubuntu sürümü? | K-01 (Server 26.04.1+ LTS) | 03-UBUNTU-BASELINE | | S-02 | Server mı Desktop mı? | K-01 (Server, GUI'siz kurulum) | 03 | | S-03 | GNOME mu XFCE mi? | K-02 (GNOME; XFCE elendi) | 03, 07 | | S-04 | Boot terminale mi grafiğe mi? | K-03 (terminal + bf-gui-on/off) | 03, 16 | | S-05 | SSH nasıl (kullanıcı, auth, root)? | K-13 (key zorunlu, root kapalı) | 06 | | S-06 | 700 cihazda tek key riski nasıl çözülür? | K-13 (per-admin key + CA hedefi) | 06 | | S-07 | RDP hangi sunucu/masaüstü ile? | K-03 (xRDP + GNOME) | 07 | | S-08 | RustDesk OSS neyi kapsar, Pro gerekir mi? | K-04 (OSS yeterli, Pro yok) | 07 | | S-09 | WireGuard çökerse uzak erişim sürer mi? | K-04 (RustDesk/MeshCentral bağımsız) | 07, 08 | | S-10 | 4. erişim kanalı hangisi? | K-04 (MeshCentral) | 07 | | S-11 | AnyDesk ücretsiz kullanılabilir mi? | K-04 (HAYIR — mimariden çıkarıldı) | 07, 23 | | S-12 | WireGuard modeli + keepalive + otomatik reconnect? | K-05 (hub-spoke, 25 sn, wg-quick enable) | 08 | | S-13 | RDP/SSH dış dünyaya açık mı? | K-05 (hayır — WireGuard arkası) | 08, 11 | | S-14 | Filo yönetimi hangi araç? | K-06 (Ansible + Semaphore Community) | 09 | | S-15 | Semaphore Pro gerekir mi? | K-06 (hayır — kanıtlı) | 09, 23 | | S-16 | Monitoring hangi yığın, 17 metrik nasıl? | K-07 (Prometheus + Kuma) | 14 | | S-17 | Çevrimdışı/last-seen nasıl izlenir? | K-07 (`up` + Push monitör) | 14 | | S-18 | Onaysız update politikası nedir? | K-11 (yasak + dalgalı onay zinciri) | 10 | | S-19 | `unattended-upgrades` ne olacak? | K-11 (imajda kapalı) | 03, 10 | | S-20 | Update dalgaları neler? | K-11 (LAB(2)/P1(5)/P2(20)/W1(50)/W2(100)/PROD) | 10, 18 | | S-21 | Docker restart politikası ne? | K-10 (`unless-stopped`) | 12 | | S-22 | `latest` / Watchtower serbest mi? | K-10 (ikisi de yasak) | 12 | | S-23 | Docker + UFW çakışması nasıl çözülür? | K-10 (localhost-bind) | 11, 12 | | S-24 | Log disiplini nedir? | K-10 + K-14 (rotasyon + textfile + bundle) | 12, 15 | | S-25 | Golden image yöntemi ne? | K-09 (autoinstall + install.sh) | 04, 05 | | S-26 | Cihaz kimliği standardı ne? | K-12 (`BF-` / `bf-`) | 02 | | S-27 | Doküman platformu hangisi? | K-08 (Docusaurus) | 22 | --- ## Ek: Karar Haritası (hangi soru hangi karara?) ```mermaid flowchart LR SORU["27 soru (Ek B)
S-01…S-27"] --> ZEMIN["Zemin: K-01/K-02/K-03
K-09/K-10/K-11/K-12"] SORU --> AG["Ağ + erişim: K-04/K-05/K-13"] SORU --> OPS["Operasyon: K-06/K-07/K-14"] SORU --> DOC["Docs: K-08"] ZEMIN --> D1["03/04/05/10/12/02"] AG --> D2["07/08/06/11"] OPS --> D3["09/14/15"] DOC --> D4["22"] ```
# 25 — Uygulama Yol Haritası (Implementation Roadmap) > Kısa özet: Blueforce 700 cihaz projesinin FAZ'lı yol haritası: FAZ 0 hazırlık → FAZ 1 merkez → FAZ 2 imaj → FAZ 3 LAB/P1 → FAZ 4 dalgalar → FAZ 5 PROD + işletme. Katılımsız provisioning (unattended) ayrı FAZ'dır; temel akış çalışmadan otomasyona geçilmez. - Dosya: `docs/25-IMPLEMENTATION-ROADMAP.md` - İlgili kararlar: `24-DECISION-LOG.md#K-09` (imaj + unattended sırası), `#K-11` (dalgalar), tüm K-01…K-14 (fazlara dağılım) - Durum: [ ] Taslak --- ## 1. Amaç Projeyi "ne, hangi sırada, hangi kapıdan geçerek" sorusuyla yönetmek. Her FAZ'ın girişi, çıkışı ve sahibi bellidir; bir FAZ kapanmadan sonrakine iş taşınmaz. ## 2. Kapsam - Kapsam içi: FAZ tanımı, sıra ve bağımlılıklar, her FAZ'ın çıkış kriteri, unattended provisioning FAZ'ının yeri, doküman-faz eşleşmesi. - Kapsam dışı: dalga cihaz sayıları detayı (18), update onay akışı (10), komut-seviyesi kurulum (03/04/05). ## 3. Kararlar ```text KARAR: Yol haritası 7 FAZ'dır: F0 Hazırlık+Envanter → F1 Merkez VDS → F2 Golden image (katılımlı) → F3 LAB+P1 → F4 Dalgalar (P2/W1/W2) → F5 PROD+İşletme → F6 Katılımsız provisioning (unattended, ayrı FAZ). GEREKÇE: Sıra bağımlılıkları zorlar: envanter olmadan imaj (K-09), imaj olmadan LAB, LAB olmadan dalga (K-11) olmaz. Unattended provisioning ayrı FAZ'dır çünkü katılımlı akış (USB→script→bayi no) sahada kanıtlanmadan PXE/preseed otomasyonu ek yük ve ek arıza yüzeyi demektir. ALTERNATİF: Unattended'i en başa koyma — temel akıştaki hata otomasyonla çarpılır; elendi. Tek-faz "hepsini yap" planı — kapısız ilerleme, filo-çapı risk; elendi. RİSK: FAZ atlama baskısı (takvim); azaltma: her FAZ çıkışı 21'deki onay yetkilisinin imzasına bağlıdır. MALİYET: Ücretsiz (işgücü planlaması; VDS kira hariç). LİSANS: Yok. ``` ## 4. Neden Bu Karar? Klasik hata sırası tersine çevirmektir: önce otomasyon, sonra temel. Bu harita bilerek "sıkıcı" sırayı seçer — önce elle çalışan, sonra kendiliğinden çalışan. F6'nın ayrı FAZ olması, unattended'in iptal edilebilir (proje yine de biter) ama atlanamaz (sırası gelmeden yapılmaz) olduğunu söyler. Her FAZ bir karar grubunu hayata geçirir; harita ile 24-DECISION-LOG arasındaki bağ tabloyla sabittir. ## 5. Alternatifler | Alternatif | Artı | Eksi | Sonuç | |---|---|---|---| | Unattended ilk FAZ | Erken otomasyon | Kanıtlanmamış akışın otomasyonu | Elendi (F6'ya taşındı) | | Az FAZ (3 büyük) | Kısa plan | Kapılar seyrek, risk büyük | Elendi | | Çok FAZ (12+) | Detaylı | Yönetim yükü, faz yorgunluğu | Elendi | ## 6. Avantajlar - Her FAZ bağımsız kapanır; proje yarıda kalsa bile kazanım (örn. F1 merkezi) kalır. - F6 opsiyoneldir: katılımlı akışla PROD bitebilir, unattended sonradan eklenir. ## 7. Dezavantajlar - FAZ disiplini yavaş görünür; paydaşa "neden F6 en sonda" açıklaması gerekir (gerekçe §4'tür). - FAZ'lar arası bekleme (gözetim süreleri) takvimi uzatır. ## 8. Riskler | Risk | Olasılık | Etki | Azaltma | |---|---|---|---| | FAZ çıkışı atlanır, sonrakine geçilir | Orta | Yüksek | Onay yetkilisi imzası zorunlu (21) | | Merkez VDS donanımı yetersiz çıkar (F1) | Orta | Orta | LAB yük testi; servisleri ikinci VDS'e bölme planı (01) | | Donanım profili imajı kırar (F2→F3) | Orta | Orta | F0 envanteri + F3 profil matrisi | ## 9. Uygulama Planı | FAZ | Ad | Kararlar | Çıkış kriteri | Dokümanlar | |---|---|---|---|---| | F0 | Hazırlık + donanım envanteri | K-12 (kimlik), K-09 (tarama-önce) | Profil listesi + envanter şeması onaylı | 02, 18 (FAZ 0), 00 | | F1 | Merkez VDS kurulumu | K-04, K-05, K-06, K-07, K-08 | WG hub + hbbs/hbbr + MeshCentral + Semaphore + Prometheus/Kuma ayakta; L9 tatbikatı yapıldı | 06, 07, 08, 09, 14, 17-L9 | | F2 | Golden image (katılımlı) | K-01, K-02, K-03, K-09, K-10, K-11 | `blueforce-install.sh` USB→READY akışı LAB donanımında çalışıyor; `unattended-upgrades` kapalı; tag/`unless-stopped` denetli | 03, 04, 05, 12 | | F3 | LAB(2) + P1(5) | K-11 (dalga), K-14 (bundle), K-05 (kopma sayacı) | 3 akış yeşil; teknisyen desteksiz kurulum yaptı; kopma sayacı toplandı | 18, 19, 20, 10 | | F4 | Dalgalar P2/W1/W2 | K-11, K-06, K-07 | Her dalga çıkış kriteri + halt kaydı; destek yükü hedefte | 18, 21, 14 | | F5 | PROD (~523) + işletmeye alma | Tümü | Filo READY; rutinler (günlük/haftalık) devrede; `v1` doküman kesildi | 21, 22, 23 | | F6 | Katılımsız provisioning (unattended) | K-09 (hedef) | Teknisyen girdisiz (veya minimum-girdili) kurulum P2 ölçeğinde kanıtlı; katılımlı akış yedekte duruyor | 04 (F6 eki) | F6 notu: F6, F5'ten sonra başlar; F5'in kapanmasına engel değildir. F6 kapsamı (PXE/preseed derinliği, ağ gereksinimleri) F3 verisine göre F4 içinde netleştirilir. F6 başarısız olursa proje F5 haliyle tamam sayılır. ```bash # örnek: FAZ kapısı kontrolü (merkez) # F3 çıkışı için LAB doğrulama seti ansible-playbook playbooks/verify-lab.yml --limit lab bf-support-bundle --out /tmp/f3-gate-bundle.tar.gz ``` ## 10. Test Planı | Test | Beklenen sonuç | Ortam | |---|---|---| | F1 L9 tatbikatı (boş VDS'e kur) | Tüm merkez servisleri ayağa kalkar | İzole VDS | | F2 imaj denetim seti (K-10/K-11 kilitleri) | `latest` yok, timer'lar maskeli, politika `unless-stopped` | LAB(2) | | F3 3-akış + rollback | Erişim/boot/update + geri alma yeşil | LAB(2)/P1(5) | | F6 unattended prova | Minimum-girdi ile READY | P2 ölçeği | ## 11. Rollback 1. FAZ içi hata: ilgili dalga/iş 17-RECOVERY veya 10-UPDATE zinciriyle geri alınır; FAZ tekrarlanır. 2. FAZ kararı yanlışsa (örn. imaj yöntemi): 24-DECISION-LOG PR'ı + bağımlı FAZ'ların tekrarı. 3. F6 geri alınırsa katılımlı akışa dönülür (yedek her zaman sıcak tutulur). ## 12. Kontrol Listesi - [ ] Her FAZ'ın çıkış kriteri + sahibi yazılı ve onaylı. - [ ] F6'nın F5 sonrası ayrı FAZ olduğu paydaşça kabul edildi. - [ ] Doküman-faz eşleşmesi (tablodaki son sütun) güncel. - [ ] Onay yetkilisi (asıl+vekil) tanımlı (21). ## 13. Açık Sorular - [ ] FAZ tarihleri ve teknisyen kapasite planı (sahibi: yönetim + 18). - [ ] F1 VDS donanım siparişi için LAB yük ölçümü (sahibi: 14-MONITORING yazarı). - [ ] F6 kapsamı (PXE şart mı, USB-preseed yeter mi) — F3 verisi sonrası (sahibi: 04 yazarı). --- ## Ek: Mermaid — FAZ akışı ```mermaid flowchart LR F0["F0 Hazırlık
+ envanter"] --> F1["F1 Merkez VDS"] F1 --> F2["F2 Golden image
(katılımlı)"] F2 --> F3["F3 LAB 2 + P1 5"] F3 --> F4["F4 P2/W1/W2"] F4 --> F5["F5 PROD + işletme"] F5 --> F6["F6 Unattended
(ayrı FAZ)"] ```
# Faz 1A Araştırma — OS / GUI / RDP + WireGuard + Docker > Durum: KAYNAK DOĞRULAMA NOTU (karar taslağı değil). Her bulgu `iddia + resmi URL + sürüm/tarih + Blueforce etkisi` formatındadır. > Doğrulama yöntemi: resmi sayfalar 2026-09-15 tarihinde doğrudan çekilerek (HTTP 200) okundu. Web arama servisi (Nous gateway) kapalıydı; bu dosyadaki hiçbir bulgu arama özetine dayanmaz, hepsi doğrudan sayfa içeriğidir. > Kural: tüm bileşenlerde FREE sürüm baz alınır (Ubuntu LTS, WireGuard, Docker CE, XFCE, xRDP). Ücretli varyantlar (Ubuntu Pro, Docker Business vb.) zorunlu mimariye konulmaz; free limitleri ilgili maddede kısaca not edilir. --- ## A1. Ubuntu 26.04 LTS var mı? Saha için uygun mu? - **BULGU A1.1 — Ubuntu 26.04.1 LTS mevcut ve resmi indirme sayfasında yayında.** - İddia: `releases.ubuntu.com` ana sayfası "Ubuntu 26.04.1 LTS (Resolute Raccoon)" başlığını, `26.04/` ve `26.04.1/` dizinlerini listeler. - Resmi URL: https://releases.ubuntu.com/ (HTTP 200, çekilme: 2026-09-15) - Sürüm/tarih: 26.04.1 LTS, kod adı Resolute Raccoon - Blueforce etkisi: Şartnamedeki "Ubuntu LTS (26.04 doğrulanacak)" maddesi DOĞRULANDI — 26.04 gerçek, güncel LTS serisidir. Ancak `.1` sürümünün yayında olması, golden image için **26.04.1+ noktasürümü** baz almayı önerir (ilk .0 dalgasındaki kurulumcu hataları genelde .1'de toplanır — bu kısım genel mühendislik kanaati, LAB'da imaj testiyle kesinleşir). - **BULGU A1.2 — 26.04 Server ISO'su varsayılan olarak GUI kurmaz.** - İddia: `26.04/` dizin sayfası Server imajını "It will not install a graphical user interface" cümlesiyle tanımlar; Desktop ve live-server ISO'ları ayrıdır. - Resmi URL: https://releases.ubuntu.com/26.04/ (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: "Server + sonradan XFCE" saha mimarisi temiz bir zemine oturur — minimal kurulum + ihtiyaca göre GUI, desteklenen standart yoldur. - **BULGU A1.3 — Ubuntu Desktop 26.04 sistem gereksinimi saha PC'leri için ağırdır.** - İddia: Resmi indirme sayfası Desktop için "2 GHz dual-core, 6 GB RAM, 25 GB disk" ister. - Resmi URL: https://ubuntu.com/download/desktop (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: 700 saha PC'sinde tam GNOME/Ubuntu Desktop standardı RAM/disk bütçesini zorlar. Bu, XFCE tercihinin en somut dayanağıdır (bkz. A2). - **BULGU A1.4 — `unattended-upgrades` Ubuntu Server'da VARSAYILAN OLARAK KURULU ve ETKİNDİR; günde bir kez çalışır.** - İddia: Resmi Server dokümantasyonu: "This is done via the `unattended-upgrades` package, which is installed by default" + "Right after installation, automatic installation of security updates will be enabled" + "By default, `unattended-upgrades` runs once per day". Kapatma yolu: `/etc/apt/apt.conf.d/20auto-upgrades` içinde `Update-Package-Lists` / `Unattended-Upgrade` değerlerini `0` yapmak; tetikleyici systemd timer'ları `apt-daily.timer` / `apt-daily-upgrade.timer`. - Resmi URL: https://ubuntu.com/server/docs/how-to/software/automatic-updates/ (HTTP 200, render; kaynak: https://raw.githubusercontent.com/canonical/ubuntu-server-documentation/main/docs/how-to/software/automatic-updates.md, HTTP 200, çekilme: 2026-09-15) - Sürüm/tarih: güncel Server dokümantasyonu (resolute dahil); man sayfası resolute'ta `unattended-upgrades 2.12ubuntu9` paketini doğrular — https://manpages.ubuntu.com/manpages/resolute/en/man8/unattended-upgrade.8.html (HTTP 200, 2026-09-15); noble paketi https://packages.ubuntu.com/noble/unattended-upgrades (HTTP 200) - Blueforce etkisi: KRİTİK — "onaysız update yasak" politikası için golden image'da `20auto-upgrades`'ın `0`'lanması ve timer'ların maskelenmesi ŞART; aksi halde 700 cihaz ilk açılışta güncellemeye kalkar (doküman ayrıca makine kapalıyken kaçan timer'ın açılışta hemen tetiklendiğini — `Persistent=true` davranışı — yazar; saha PC'leri sık kapanan makineler olduğu için bu not runbook'a girer). Free notu: ESM kapsamı Pro'ya girer — mimaride ESM'e bel bağlanmaz, standart `security` cebi yeterlidir (ücretsiz). --- ## A2. Masaüstü: XFCE vs GNOME + xRDP (700 ölçek) - **BULGU A2.1 — Xfce resmi olarak "hafif, hızlı, düşük sistem kaynağı" hedefler.** - İddia: Proje ana sayfası: "Xfce is a lightweight desktop environment… It aims to be fast and low on system resources." - Resmi URL: https://www.xfce.org/about (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: GNOME (6 GB RAM şartı, A1.3) karşısında XFCE'nin saha standardı olması yönünde karar verilir. RDP üzerinden 700 cihaza bakımda düşük RAM/CPU ayak izi = daha az donma, daha ucuz saha donanımı. - **BULGU A2.2 — xRDP aktif, güncel, ücretsiz (Apache-2.0) RDP sunucusudur; Temmuz 2026 sürümü var.** - İddia: GitHub `neutrinolabs/xrdp`: açıklama "xrdp: an open source RDP server", son push 2026-09-09, son sürüm v0.10.6.1 (2026-07-07), ~6728 yıldız, varsayılan dal `devel`. - Resmi URL'ler: https://github.com/neutrinolabs/xrdp + https://api.github.com/repos/neutrinolabs/xrdp (+ `/releases/latest`) (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: xRDP "ölü proje" riski YOK — 4. erişim yöntemi adayı olarak mimaride kalır. Lisans ücretsiz, 700 ölçekte lisans maliyeti doğmaz. - **BULGU A2.3 — xRDP mimarisi oturum yöneticisi (sesman) + servis birimleri üzerine kuruludur; GUI aç/kapa buradan türetilir.** - İddia: Upstream README kaynak ağacı `sesman` (session manager), `xrdp-sesman.service.in` ve `xrdp.service.in` systemd birimlerini içerir (devel dalı `instfiles/` listesi ve README'den doğrulandı). - Resmi URL: https://github.com/neutrinolabs/xrdp (README + `instfiles/`, `sesman/` dizinleri; GitHub API HTTP 200, 2026-09-15) - Blueforce etkisi: `bf-gui-on/off` tasarımının dayanağı: GUI, display-manager servisi + xRDP servisleri (`xrdp`, `xrdp-sesman`) üzerinden systemd ile açılıp kapatılabilir bir katman olarak modellenir (kesin birim adları ve XFCE oturum başlatma satırı LAB'da 26.04.1 imajında doğrulanacak — bu dosyada komut uydurulmaz). - **DOĞRULANAMAYAN / LAB'A BIRAKILAN:** xRDP GitHub wiki'si JS ile yükleniyor, statik çekmede oturum-masaüstü uyumluluk tablosu okunamadı (https://github.com/neutrinolabs/xrdp/wiki, 2026-09-15). GNOME-üzerinde-xRDP'nin bilinen ek ayar ihtiyacı bu kaynaktan teyit EDİLEMEDİ → "XFCE birincil, GNOME denenmeyecek" kararı A1.3+A2.1'e dayanır; GNOME+xRDP kombinasyonu LAB gündemine "test edilmeyecek alternatif" olarak değil, "XFCE yetersiz kalırsa" yedeği olarak yazılır. --- ## A3. WireGuard otomatik reconnect (Turkcell NAT arkası saha) - **BULGU A3.1 — NAT arkasındaki eşin erişilebilir kalmasının resmi mekanizması PersistentKeepalive'dır; önerilen değer 25 sn.** - İddia: Resmi QuickStart "NAT and Firewall Traversal Persistence" bölümü: NAT/güvenlik duvarı arkasındaki eşin bağlantı eşlemesini canlı tutmak için periyodik keepalive göndermesi gerekir; "A sensible interval that works with a wide variety of firewalls is 25 seconds"; `0` = kapalı (varsayılan); ayar `PersistentKeepalive =` alanı veya `persistent-keepalive` komut satırı ile yapılır; çoğu kullanıcının ihtiyacı yoktur, protokolü biraz daha geveze yapar. - Resmi URL: https://www.wireguard.com/quickstart/ (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: Saha PC'leri (mobil NAT arkası) için istemci konfigürasyonunda `PersistentKeepalive = 25` STANDART olur. Sunucu tarafında keepalive gerekmez (simetrik değil — istemci canlı tutar). - **BULGU A3.2 — `wg-quick@.service` systemd birimi Ubuntu paketinin içinden gelir; enable = açılışta otomatik VPN.** - İddia: `wireguard-tools` (noble/amd64) dosya listesi `/lib/systemd/system/wg-quick@.service` ve `wg-quick.target` içerir → `systemctl enable wg-quick@wg0` kalıcı açılır; resolute (26.04) paket sürümü `1.0.20250521-1ubuntu1`. - Resmi URL'ler: https://packages.ubuntu.com/noble/amd64/wireguard-tools/filelist ve https://packages.ubuntu.com/resolute/wireguard-tools (HTTP 200, 2026-09-15) - Blueforce etkisi: "Otomatik reconnect" iki katmandan kurulur: (1) WireGuard keepalive (A3.1) + (2) systemd `wg-quick@wg0` enable + restart. Elektrik kesintisi sonrası cihaz açılınca VPN kendiliğinden gelir — 16-POWER-LOSS dokümanının ağı ayağı hazır. - **DOĞRULANAMAYAN / SAHADA ÖLÇÜLECEK:** "Turkcell CGNAT" özelliği resmi dokümandan doğrulanamaz (operatör bilgisi). Tasarım, operatörden bağımsız NAT-traversal'a (A3.1) dayanır; PİLOT-1'de her cihazda `PersistentKeepalive=25` ile 7/24 kopma sayacı tutulur, keepalive aralığı saha verisine göre ayarlanır. Tahminle 10 sn gibi agresif değer yazılmaz. --- ## A4. Docker (CE, ücretsiz): UFW bypass + log + restart + latest + Watchtower - **BULGU A4.1 — Docker, yayımlanan portlarda UFW'yi BAYPAS eder; bu resmi olarak belgelenmiştir.** - İddia: Resmi doküman "Docker and ufw": Docker ve UFW uyumsuz çalışır; konteyner portu yayımlanınca trafik `nat` tablosunda yönlendirilir, UFW'nin kullandığı INPUT/OUTPUT zincirlerine uğramadan geçer — "effectively ignoring your firewall configuration". - Resmi URL: https://docs.docker.com/engine/network/packet-filtering-firewalls/ (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: KRİTİK — MEG ve yardımcı servisler `ports:` ile yayımlanırsa UFW kuralları delinir. Güvenli çözüm seçenekleri (dokümanın aynı sayfasından): (a) port yayımlamayı 127.0.0.1'e bağlamak, (b) `iptables:false` (doküman "çoğu kullanıcı için uygun değil, konteyner ağını bozabilir" diye uyarır — bu yüzden varsayılan çözüm DEĞİL), (c) Docker'ın `DOCKER-USER`/nftables belgeli zincirleri. Saha standardı: (a) + WireGuard üzerinden erişim; detay `12-DOCKER-OPERATIONS` ve `11-SECURITY-HARDENING`'de. - **BULGU A4.2 — Varsayılan `json-file` log sürücüsü ROTASYON YAPMAZ; disk dolmasına yol açar. Çözüm `max-size`/`max-file` daemon ayarıdır.** - İddia: Resmi doküman: "By default, no log-rotation is performed… which can lead to disk space exhaustion"; örnek daemon ayarı `{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }`; `max-file` gibi sayısal değerler string yazılır; ayar değişikliği sonrası Docker restart gerekir ve sadece yeni konteynerlere uygulanır. - Resmi URL: https://docs.docker.com/engine/logging/configure/ (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: Golden image'daki `/etc/docker/daemon.json`'da log rotasyonu ZORUNLU standart olur (saha PC'leri aylarca dokunulmaz; dolan disk = kilitlenen cihaz). Değerler (10m/3) başlangıç önerisi; MEG log hacmi PİLOT'ta ölçülüp ayarlanır. - **BULGU A4.3 — Saha standardı restart politikası `unless-stopped`'tur.** - İddia: Resmi doküman: `unless-stopped` "`always`'e benzer, ancak konteyner durdurulmuşsa (manuel ya da başka şekilde) Docker daemon yeniden başlasa bile yeniden başlatılmaz". Politika ancak konteyner en az bir kez başarıyla başladıktan sonra devreye girer. - Resmi URL: https://docs.docker.com/engine/containers/start-containers-automatically/ (HTTP 200, çekilme: 2026-09-15) - Blueforce etkisi: `always` yerine `unless-stopped` seçilir — saha teknisyeni bakım için durdurduğu konteynerin reboot sonrası sürpriz şekilde kalkmaması gerekir (öngörülebilirlik > her koşulda ayakta tutma). Elektrik kesintisi kurtarması `unless-stopped` ile aynen çalışır (kesinti "manuel stop" sayılmaz). - **BULGU A4.4 — `latest` etiketi yasaklanır (determinizm gerekçesi).** - İddia: Docker Hub etiket-semantiği sayfası taşınmış, resmi cümle bu turda yakalanamadı (https://docs.docker.com/docker-hub/repos/manage/hub-images/ HTTP 200 ama `latest` geçmiyor, 2026-09-15). Bu yüzden yasak, resmi alıntıya değil Blueforce "onaysız update yasak" politikasına dayandırılır: hareketli etiket, hangi imajın koştuğunu belirsizleştirir; 700 cihazda aynı imajı garanti etmenin yolu sabit sürüm etiketi (+ mümkünse digest) ve merkezi rollout'tur. - Blueforce etkisi: Compose dosyalarında `image: …:latest` YASAK; sabit tag zorunlu. Digest pinleme (`@sha256:…`) PİLOT-2'de değerlendirilecek ek sıkılık adımı olarak açık soruya yazılır (komut/sözdizimi uydurulmadı, doküman yazımında Compose spec'ten ayrıca doğrulanacak). - **BULGU A4.5 — Watchtower kullanılmaz; iki bağımsız gerekçe.** - İddia 1 (politika): Watchtower "otomatik imaj güncelleyici"dir — Blueforce "onaysız update yasak" kuralını doğrudan ihlal eder (LAB→PİLOT→WAVE rollout zincirini bypass eder). - İddia 2 (upstream): `containrrr/watchtower` deposu ARŞİVLENMİŞTİR (`archived: true`, son push 2025-12-17, ~24.6k yıldız, 219 açık issue). - Resmi URL: https://github.com/containrrr/watchtower + https://api.github.com/repos/containrrr/watchtower (HTTP 200, 2026-09-15) - Blueforce etkisi: Watchtower mimariye GİRMEZ; güncellemeler Ansible/Semaphore zincirinden, onaylı ve dalgalı yapılır. Alternatif "unattended imaj çekme" fikri de aynı gerekçeyle reddedilir. - Free notu: Docker CE ücretsizdir; Docker Business/Scout gibi ücretli özellikler mimaride yoktur ve gerekmez. --- ## Karar etkisi özeti (Faz 2'ye girdi) | # | Karar yönü | Dayanak bulgu | |---|-----------|---------------| | 1 | Baz OS: Ubuntu Server 26.04.1+ LTS (GUI'siz kurulum, XFCE sonradan) | A1.1, A1.2 | | 2 | Masaüstü: XFCE birincil; tam GNOME saha standardı değil | A1.3, A2.1 | | 3 | Uzaktan bakım katmanı: xRDP (aktif upstream, ücretsiz) + systemd ile GUI aç/kapa | A2.2, A2.3 | | 4 | `unattended-upgrades` imajda kapatılır (timer dahil), update zinciri LAB→…→PROD | A1.4 | | 5 | WireGuard istemci: `PersistentKeepalive=25` + `wg-quick@wg0` enable | A3.1, A3.2 | | 6 | Docker: UFW-bypass'a karşı localhost-bind standardı; log rotasyonu zorunlu; `unless-stopped`; `latest` yasak; Watchtower yok | A4.1–A4.5 | Açık sorular (tahmin yazılmaz, LAB/PİLOT'ta ölçülür): XFCE oturum başlatma satırı ve `bf-gui-on/off` kesin birim listesi (26.04.1 imajında); MEG log hacmine göre `max-size/max-file` ayarı; Turkcell NAT kopma sayacı ve keepalive ayarı; digest pinleme kararı. # B — Uzaktan Erişim Araştırması (Faz 1B) Tarih: 2026-09-15. Kapsam: 700 cihazlık filo için bağımsız uzaktan erişim kanalı. Sözlük: hbbs = RustDesk ID/randevu (rendezvous) + sinyalizasyon sunucusu; hbbr = röle (relay) sunucusu; VDS = sanal özel sunucu. > Not: Web arama altyapısı (Nous gateway) kapalı olduğundan bulgular > curl ile çekilen resmi dokümantasyon sayfaları + GitHub API üzerinden > doğrulandı. Cloudflare korumalı `anydesk.com` pazarlama sayfalarına curl > ile erişilemedi; AnyDesk fiyatlandırması gerçek tarayıcı ile, lisans > şartları ise Cloudflare korumasız resmi bilgi bankası > (`support.anydesk.com`) üzerinden doğrulandı. ## 1. RustDesk OSS self-hosted (hbbs + hbbr) - Mimari: istemciler önce hbbs'e kaydolur (ID, heartbeat, NAT tipi testi); doğrudan P2P kurulamazsa trafik hbbr üzerinden rölelenir. İkisi de tek `rustdesk/rustdesk-server` imajından `hbbs` / `hbbr` komutuyla çalışır. Kaynak: https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ (erişim 2026-09-15; sürüm bilgisi aşağıda). - VDS kurulumu (Docker Compose, önerilen `network_mode: "host"`): `rustdesk/rustdesk-server:latest` imajı, `./data:/root` kalıcı hacmi, `unless-stopped` yeniden başlatma. Kaynak: https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/docker/ (erişim 2026-09-15). - Güvenlik duvarı portları: hbbs 21115/TCP (NAT testi), 21116/TCP+UDP (ID kaydı/heartbeat + TCP hole punching), 21118/TCP (web istemcisi, opsiyonel); hbbr 21117/TCP (röle), 21119/TCP (web istemcisi, opsiyonel). 21114/TCP yalnızca Pro web konsolunundur. 21118/21119 kullanılmıyorsa kapalı tutulmalıdır (IP sahteciliği uyarısı dokümanda). Kaynak: aynı Docker sayfası (erişim 2026-09-15). - Pro gerektirmeyen özellikler (OSS'ta mevcut): sınırsız istemci, kendi ID + röle altyapın, uçtan uca şifreli P2P/röleli oturum, dosya transferi, katılımsız erişim (kalıcı parola), betikli toplu dağıtım (PowerShell / batch / MSI / macOS bash / Linux bash). Toplu dağıtımda istemciye self-hosted sunucu adresi + yapılandırma dizesi (config string) gömülür. Kaynak: https://rustdesk.com/docs/en/self-host/client-deployment/ (erişim 2026-09-15). - Yalnızca Pro'da olanlar (OSS'ta YOK): web konsolu, API, OIDC/SSO, LDAP, 2FA, erişim kontrolü, rol tabanlı yönetim, denetim logları, strateji/ adres defteri, özel istemci üretici. Kaynak: https://rustdesk.com/docs/en/self-host/rustdesk-server-oss/ + https://rustdesk.com/pricing/ (erişim 2026-09-15; Individual ~11,88 USD/ay, Basic ~23,88 USD/ay, yıllık faturalı). - Sürümler (GitHub API, 2026-09-15): istemci `rustdesk/rustdesk` 1.4.9 (2026-07-06); sunucu `rustdesk/rustdesk-server` 1.1.16 (2026-07-20). Lisans: AGPL-3.0 (her iki repo). Yıldız: istemci ~123,6b, sunucu ~10,4b. ### WireGuard çökerse bağımsız kanal çalışır mı? — EVET RustDesk istemcisi, yapılandırılan ID sunucusuna (VDS genel IP/DNS, 21115-21117) doğrudan internet üzerinden ulaşır; trafik WireGuard arayüzünden geçme zorunluluğu yoktur (istemcide tünel içi bir adres değil, VDS'nin gerçek adresi tanımlanır). hbbs/hbbr, istemcilerin gerçek gelen IP'sini görmelidir (`--net=host` önerisinin gerekçesi de budur). Dolayısıyla WireGuard servisi veya ana tünel çökse bile RustDesk kanalı ayakta kalır; tek ortak bağımlılık VDS'nin kendisi ve genel internet erişimidir. Bu, tasarım hedefiyle de tutarlıdır: "kendi randevu/röle sunucunu kur, verinin kontrolü sende" (https://github.com/rustdesk/rustdesk README, erişim 2026-09-15). Öneri: Blueforce mimarisinde RustDesk kanalını WireGuard'a bağımlı yapmayacak şekilde, istemcide VDS genel adresi kullan; `ALWAYS_USE_RELAY=Y` yalnız NAT sorunlu sahalarda değerlendir (gecikme/ bant genişliği maliyetiyle). ## 2. AnyDesk ticari lisans — 700 kurumsal cihazda ücretsiz kullanım yasal mı? **HAYIR.** Ücretsiz sürüm yalnızca kişisel kullanımı kapsar; ticari kullanımda giden bağlantıyı başlatan istemcilerin lisanslı olması şarttır. Resmi bilgi bankası: "Only outgoing clients that initiate connections to other AnyDesk clients for commercial use need to have an AnyDesk license." Kaynak: https://support.anydesk.com/docs/anydesk-licenses.md (güncellenme 2025-10-30, erişim 2026-09-15). 700 cihazlık kurumsal filo açıkça ticari kullanımdır; ücretsiz sürüm lisans ihlali olur. - Güncel fiyatlar (https://anydesk.com/en/pricing, tarayıcıyla erişim 2026-09-15): Solo (100 yönetilen cihaz) → Standard ~34,32 USD/ay (500 yönetilen cihaz, 20 kullanıcı) → Advanced ~72,72 USD/ay (1000 yönetilen cihaz, 100 kullanıcı) → Ultimate (2000 cihazdan başlar, ölçeklenebilir, teklifli). 700 cihaz Standard'ı aşar (500) → en az Advanced gerekir; hepsi yıllık faturalı, KDV hariç. - **Mimari karar: AnyDesk birincil/ücretsiz kanal olarak mimariden ÇIKARILDI.** Gerekçe: lisans maliyeti + kapalı kaynak + self-hosted seçeneğin yalnızca en üst (Ultimate/On-Premises) planda olması. - Opsiyonel not (mimari dışı): müşteri sahasında zaten lisanslı AnyDesk varsa, teknisyen erişimi için "opsiyonel installer modülü" olarak paketlenebilir; ancak Blueforce imajında varsayılan kurulu gelmez, envanter/lisans takibi müşteriye aittir. ## 3. Dördüncü bağımsız ücretsiz erişim adayı: MeshCentral (önerilen) Tailscale/ZeroTier gibi freemium, merkezi kimlik isteyen çözümler elendi; gerçek OSS + self-hosted olan MeshCentral seçildi. - Nedir: NodeJS tabanlı, web üzerinden bilgisayar yönetimi; aracı (agent) kurulu cihazlar sunucuda görünür; web tabanlı uzak masaüstü + terminal + dosya yönetimi dahili. Kaynak: https://meshcentral.com (erişim 2026-09-15) + https://github.com/Ylianst/MeshCentral readme.md (erişim 2026-09-15). - Lisans: Apache-2.0 (package.json `license` alanı + LICENSE dosyası, GitHub API ile doğrulandı 2026-09-15). Ticari kullanım dahil ücretsiz, self-hosted'da kullanıcı/cihaz kotası yok. - Sürüm: 1.2.5 (yayınlanma 2026-08-12, GitHub API 2026-09-15). Aktif bakımda (son push 2026-09-14). Yıldız ~7,2b. - Platformlar: sunucu ve aracı Windows/Linux/macOS/FreeBSD (resmi site). Kurulum: `meshcentral` npm paketi üzerinden (`npm install -g meshcentral` standardı), dokümantasyon: https://ylianst.github.io/MeshCentral/ (README'de bağlantılı, erişim 2026-09-15). - Blueforce rolü: RustDesk birincil grafik kanal; MeshCentral ikincil yönetim düzlemi (envanter + terminal + dosya + web konsolu) olarak aynı veya ayrı VDS'de koşar. O da kendi portları üzerinden internete çıkar; WireGuard'dan bağımsız ikinci bir "ölüm-dışı" kanal sağlar. Not: ilk kurulumda TLS (Let's Encrypt / ters vekil) ve 2FA açılmalıdır (resmi dokümanda anlatılır). ## 4. Karar özeti | Kanal | Rol | Lisans | WireGuard bağımsız mı? | |---|---|---|---| | RustDesk OSS (hbbs/hbbr, VDS) | Birincil grafik uzak erişim | AGPL-3.0, ücretsiz | EVET | | MeshCentral (VDS) | İkincil yönetim (terminal/dosya/envanter) | Apache-2.0, ücretsiz | EVET | | WireGuard | Ana güvenli ağ | OSS | — (kendisi) | | AnyDesk | Mimariden çıkarıldı; yalnızca müşteri lisanslıysa opsiyonel installer modülü | Ticari lisans gerekli | — | Doğrulanamayan / sonraki faza kalan: RustDesk `id_ed25519.pub` anahtar dağıtım otomasyonu detay adımları (Faz 2 kurulum betiklerinde ele alınacak); MeshCentral gerçek VDS kurulum testi yapılmadı (Faz 2). # Faz 1C Araştırma — Fleet Yönetimi + Monitoring + Docs Platformu - Tarih: 2026-09-15 (doğrulama günü güncel resmi kaynaklar) - Kapsam: 700 saha cihazı, %100 ücretsiz/self-hosted, sadelik öncelikli - Kural: Her bulgunun resmi URL'si vardır. Sürüm/plan bilgisi değişebilir; karar öncesi URL'den teyit edilir. --- ## 1. Fleet Yönetimi Karşılaştırması ### 1.1. Adaylar ve lisans (resmi kaynak) | Araç | Lisans (OSS çekirdek) | Kaynak | |---|---|---| | Ansible (CLI / core) | GPL-3.0 | https://github.com/ansible/ansible | | Semaphore UI Community | MIT, "$0, free forever, open source" | https://github.com/semaphoreui/semaphore — https://semaphoreui.com/pricing | | AWX | Apache-2.0 | https://github.com/ansible/awx (README rozeti → https://github.com/ansible/awx/blob/devel/LICENSE.md) | | Salt (Salt Project) | Apache-2.0 | https://github.com/saltstack/salt — https://docs.saltproject.io/ | | Rudder (Core) | GPL-3.0, Core açık kaynak; Enterprise abonelik node başına/yıl ücretli | https://github.com/Normation/rudder — https://www.rudder.io/en/pricing/ | | MeshCentral | Apache-2.0 | https://github.com/Ylianst/MeshCentral — https://meshcentral.com/ | ### 1.2. Semaphore UI Community — ücretsiz/OSS özellikler ve Pro bağımlılığı yoktur kanıtı Resmi plan karşılaştırmasına göre (https://semaphoreui.com/pricing): **Community ($0) içinde VAR (ücretsiz, MIT, self-hosted):** Ansible/Terraform/OpenTofu/Shell/Python/PowerShell çalıştırma; статиk/dosya/dinamik envanter yönetimi; yeniden kullanılabilir Task Template; Cron zamanlama; Variable Groups; merkezi Key Store (şifreli, runtime enjeksiyon); Global Runner ile dağıtık çalıştırma; proje-seviyesi erişim kontrolü + RBAC; Task/Activity/Server log geçmişi; REST API; Webhook tetikleyiciler; Git depoları (GitHub/GitLab/Bitbucket); bildirimler (e-posta, Slack, Teams vb.); AI iş akışları için MCP sunucusu; docs + topluluk desteği. **Yalnızca Pro/Enterprise'da VAR (Community'de YOK, ama 09-FLEET kapsamı için gerekli DEĞİL):** izole Project Runner'lar; task özetleri; HashiCorp Vault/AWS/Azure/Devolutions gizli yönetimi; 2FA (TOTP); LDAP/AD; OIDC SSO; IdP grup eşleme; özel roller; dosya-tabanlı log dışa aktarma; HA kurulum; air-gapped lisans; SLA destek. **Pro bağımlılığı yok — üç kanıt:** 1. Community "free forever and open source ... start automating without a paid subscription" — https://semaphoreui.com/pricing 2. Community kodu MIT lisanslı public repo — https://github.com/semaphoreui/semaphore 3. Kritik eşik: **Pro en fazla 500 managed node** destekler ("Up to 500 managed nodes ... For larger environments, choose Enterprise"). 700 cihaz zaten Pro kapsamını aşıyor; ücretli yola girilse bile Pro yetmez, Enterprise gerekir. Community'de lisansla dayatılan node kotası yoktur (self-hosted, kota lisans anahtarına bağlıdır; Community anahtarsız çalışır). Sonuç: filo yönetimi Community + Ansible CLI ile kurulur; hiçbir iş akışı Pro/Enterprise özelliğine bağlanmaz (Vault yerine Key Store, SSO yerine WireGuard arkası + SSH anahtarı, izole runner yerine Global Runner). **Community free limitleri (bilinerek kabul edilenler, Pro'ya taşınmadan çözümüyle):** - OIDC/SSO, LDAP/AD, 2FA yok → çözüm: Semaphore yalnızca WireGuard arkasında, erişim VPN + SSH anahtarı ile. - HashiCorp Vault/AWS/Azure gizli yönetimi yok → çözüm: yerleşik şifreli Key Store. - İzole Project Runner yok → çözüm: Global Runner + proje-seviyesi erişim kontrolü. - Dosya-tabanlı log dışa aktarma ve task özeti yok → çözüm: Task/Activity geçmişi UI + API'den çekilir, merkezi loga taşınır. - HA yok → çözüm: Ansible CLI bağımsız çalışır; Semaphore çökse filo durmaz (günlük yedek + hızlı yeniden kurulum). ### 1.3. Karşılaştırma — 700 cihaz sadeliği | Kriter | Ansible CLI | Semaphore Community | AWX | Salt | Rudder Community | MeshCentral | |---|---|---|---|---|---|---| | 700 cihaza ek yük | Yok (agent'sız, SSH) | Tek Go binary + DB, hafif | **Ağır: Kubernetes + AWX Operator zorunlu** | Minion agent + master PKI | Server + relay + agent (ağır) | Server + agent (uzaktan erişim ağırlıklı) | | Öğrenme/işletme sadeliği | YAML playbook, en basit | Playbook'u UI'dan çalıştırma, basit | K8s bilgisi ister, karmaşık | State/pillar/grain modeli, orta-zor | Politika/direktif modeli, orta-zor | Filo otomasyonu değil, erişim aracı | | Ücretsiz kapsam riski | Yok (GPL-3.0) | Yok (yukarıdaki kanıt) | **Sürüm riski: son release 2 Tem 2024, "releases paused during refactoring"** — https://github.com/ansible/awx | SaltStack Config (Broadcom) ücretli; OSS taraf güvenli ama master ölçekleme ister | Enterprise modülleri (relay, patch kampanyası, CVE yönetimi) ücretli — https://www.rudder.io/en/pricing/ | Yok (Apache-2.0), ama konfigürasyon yönetimi değil | | 700 cihaz kanıtı | Binlerce node pratiği yaygın | Global Runner + dinamik envanter | Ölçeklenir ama K8s maliyetiyle | Ölçeklenir (master yatırımı) | Ölçeklenir (relay yatırımı) | 700 cihaza filo-komut modeli zayıf | - AWX kurulumu: https://docs.ansible.com/projects/awx/en/latest/ (Operator tabanlı) ve refactoring duyuruları: https://www.ansible.com/blog/upcoming-changes-to-the-awx-project/ - Salt docs: https://docs.saltproject.io/ — minion/master mimarisi 700 cihazda ek PKI ve master kapasite planı ister. - MeshCentral kapsamı "remote monitoring and management" (erişim + envanter), Ansible-filo karşılığı değildir: https://github.com/Ylianst/MeshCentral ### 1.4. KARAR - **KARAR:** Birincil filo otomasyonu **Ansible CLI (SSH, agent'sız)**; operatör arayüzü **Semaphore UI Community (self-hosted, MIT)**. Hiçbir playbook/iş akışı Pro/Enterprise özelliği kullanmaz. - **GEREKÇE:** En düşük hareketli parça (SSH + YAML + tek Go binary); 700 cihazda ek agent/master/K8s/DB maliyeti yok; Community ücretsizliği resmi fiyat sayfasıyla kanıtlı; 500-node Pro kotası 700 cihazda ücretli yolu zaten kapatıyor. - **ALTERNATİF:** AWX (K8s yükü + release'ler duraklatıldı → elendi); Salt (agent + master PKI yükü → elendi); Rudder Community (politika modeli güçlü ama relay/Enterprise modülleri ve öğrenme eğrisi → elendi); MeshCentral (4. erişim kanalı adayı olarak değerlendirilir, filo otomasyonu olarak değil). - **RİSK:** Semaphore tek nokta arızası → Ansible CLI her zaman bağımsız çalışır (UI çökse filo durmaz). Community'de OIDC/2FA yok → Semaphore yalnızca WireGuard arkasında yayınlanır, admin erişimi VPN + SSH anahtarı ile. - **MALİYET:** $0 (kendi VDS'i üzerinde). - **LİSANS:** Ansible GPL-3.0, Semaphore MIT — ticari kullanımda lisans bedeli yok. --- ## 2. Monitoring Karşılaştırması ### 2.1. Adaylar ve lisans | Çözüm | Lisans | Kaynak | |---|---|---| | Prometheus + Node Exporter | Apache-2.0 / Apache-2.0 | https://github.com/prometheus/prometheus — https://github.com/prometheus/node_exporter — https://prometheus.io/docs/guides/node-exporter/ | | Grafana OSS | AGPL-3.0 (self-hosted ücretsiz) | https://github.com/grafana/grafana — https://grafana.com/oss/ | | Netdata (Agent OSS + Cloud) | Agent GPL-3.0; **Cloud Free yalnızca 5 node'a kadar, sonrası node başına ~$4.50/ay'dan başlar** | https://github.com/netdata/netdata — https://www.netdata.cloud/pricing/ | | Uptime Kuma | MIT | https://github.com/louislam/uptime-kuma | ### 2.2. Karşılaştırma — 700 cihaz | Kriter | Prometheus + Node Exporter + Grafana OSS | Netdata | Uptime Kuma | |---|---|---|---| | Merkezi metrik (700 node) | Sınırsız, ücretsiz (kendi Prometheus'u) | Merkezi görünüm Cloud'da; **ücretsiz kota 5 node** → 700 cihaz ücretli | Metrik yok; sadece uptime/nabız | | Cihaz başı yük | node_exporter tek binary (~hafif, :9100) | Agent zengin ama 700'de Parent/Cloud planı gerekir | Yok (ajan gerektirmez; push/ping) | | Kurulum sadeliği | Orta (prometheus.yml + systemd) | Kolay (agent), ama 700'de merkezi tasarım gerekir | Çok kolay (tek Docker konteyneri) | | Alarm | Alertmanager / Grafana alerting (ücretsiz) | Cloud bildirimleri kotaya bağlı | 90+ bildirim servisi, ücretsiz, 20 sn aralık — https://github.com/louislam/uptime-kuma | | Last-seen/çevrimdışı takibi | `up` metriği + `time() - timestamp` | Agent kalp atışı (kotalı merkez) | Push monitörü + ping grafik + sertifika bilgisi | ### 2.3. 17 minimum metrik — nerede, nasıl toplanır Önerilen mimaride (Prometheus birincil + Uptime Kuma tamamlayıcı) her metriğin kaynağı: | # | Metrik | Prometheus + Node Exporter | Uptime Kuma | Netdata Community | |---|---|---|---|---| | 1 | online/offline | `up{job="node"}` (scrape başarısı) | HTTP/TCP/Ping monitörü UP/DOWN | agent kalp atışı | | 2 | last seen (son görülme) | `time() - timestamp(up)` veya Pushgateway/nodeli `node_time_seconds` farkı | Push monitör zaman damgası | — (kotalı) | | 3 | CPU % | `node_cpu_seconds_total` → rate | — | agent | | 4 | RAM % | `node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes` | — | agent | | 5 | Disk % (her mount) | `node_filesystem_avail_bytes / node_filesystem_size_bytes` | — | agent | | 6 | Disk inode % | `node_filesystem_files_free / node_filesystem_files` | — | agent | | 7 | Disk IO (read/write) | `node_disk_read_bytes_total`, `node_disk_written_bytes_total` | — | agent | | 8 | Ağ up/down (B/sn) | `node_network_receive_bytes_total`, `node_network_transmit_bytes_total` | ping gecikme grafiği (kısmi) | agent | | 9 | Load average (1/5/15) | `node_load1/5/15` | — | agent | | 10 | Uptime / boot zamanı | `node_boot_time_seconds`, `node_time_seconds - node_boot_time_seconds` | — | agent | | 11 | Swap kullanımı | `node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes` | — | agent | | 12 | systemd failed üniteler | `node_systemd_unit_state{state="failed"}` (systemd collector) | — | agent | | 13 | Sıcaklık (CPU/disk) | `node_hwmon_temp_celsius` / `node_thermal_zone_temp` (textfile ile de beslenebilir) | — | agent/eBPF | | 14 | WireGuard handshake yaşı | textfile collector (`wg show all latest-handshakes` → `.prom`) | Ping monitörü (dolaylı: VPN çökerse DOWN) | — | | 15 | Docker konteyner durumu (MEG dahil) | cAdvisor veya textfile (`docker ps` özeti → `.prom`) | Docker konteyner monitörü (var/yok) | agent | | 16 | MEG süreç sağlığı | textfile (`pgrep`/health endpoint → `.prom`) + blackbox HTTP probu | HTTP(s) Keyword monitörü (MEG arayüzü kelime kontrolü) | — | | 17 | Ping gecikmesi / paket kaybı | blackbox_exporter `probe_duration_seconds`, `probe_success` | Ping grafiği (yerleşik) | — | - Node Exporter collector'ları ve `:9100/metrics` davranışı: https://prometheus.io/docs/guides/node-exporter/ - textfile collector (özel metrikler: WireGuard, Docker, MEG için standart yol): Node Exporter `--collector.textfile.directory` (https://github.com/prometheus/node_exporter). - Uptime Kuma yetenekleri (Docker konteyner, Push, HTTP Keyword, ping grafiği, 20 sn aralık, 90+ bildirim): https://github.com/louislam/uptime-kuma - Netdata ücretsiz kotası nedeniyle 3–17'nin merkezi takibi 700 cihazda ücrete takılır: https://www.netdata.cloud/pricing/ **Free sürüm limit notları (monitoring):** - Prometheus + Node Exporter: limitsiz ve kotasız (Apache-2.0); limit, donanım kapasitesidir (retention/kardinalite LAB'da ölçülür). - Grafana OSS (AGPL-3.0, self-hosted ücretsiz): dashboard, alerting, LDAP/OAuth dahil; Enterprise'a özgü (SAML, audit log, datasource izinleri, scaling desteği) özellikler kapsam dışı bırakıldı — 14-MONITORING yalnızca OSS özelliği kullanır (https://grafana.com/oss/). - Netdata: Agent OSS (GPL-3.0) tek başına kotasızdır, ancak 700 cihazın **merkezi** görünümü Cloud üzerinden kotaya takılır (Free: 5 node; sonrası node başına ücret). Self-hosted Parent ile merkezileştirme mümkün olsa da ek tasarım/işletme yükü getirir → elenme gerekçesi budur. - Uptime Kuma (MIT): tüm özellikler ücretsiz, yerleşik limit yok; tek sınır, metrik toplamamasıdır (uptime nabzı verir). ### 2.4. KARAR - **KARAR:** Birincil **Prometheus + Node Exporter + Grafana OSS** (tüm 17 metrik); tamamlayıcı **Uptime Kuma** (saha-teknisyeni dostu UP/DOWN panosu + Push "last seen" + bildirimler). Netdata elendi. - **GEREKÇE:** 17 metriğin tamamı yalnızca Prometheus yığınında ücretsiz ve kotasız toplanır (14/15/16 textfile ile); Netdata Cloud Free 5 node kotası 700 cihazda ücrete düşer; Uptime Kuma metrik vermez ama en basit çevrimdışı alarmını ve durum sayfasını verir — ikisi birbirini tamamlar, karmaşıklık artmaz (Uptime Kuma tek konteyner). - **ALTERNATİF:** Yalnızca Uptime Kuma (metrik körlüğü → elendi); Netdata self-hosted Parent (merkezi tasarım + kota riski → elendi). - **RİSK:** 700 node'lu Prometheus'ta saklama/kardinalite → kayıt aralığı 60 sn, retention 30–90 gün, etiket şeması `BF-` standardında sabitlenir; federasyon/fazlı rollout ile büyütülür. - **MALİYET:** $0 (kendi VDS'i üzerinde). - **LİSANS:** Prometheus/Node Exporter Apache-2.0; Grafana OSS AGPL-3.0 (değişiklik yapılmadan self-host kullanılır); Uptime Kuma MIT. --- ## 3. Docs Platformu Karşılaştırması ### 3.1. Adaylar ve lisans | Platform | Lisans | Kaynak | |---|---|---| | Docusaurus | MIT | https://github.com/facebook/docusaurus — https://docusaurus.io/ | | MkDocs (+ Material) | BSD-2-Clause (MkDocs); Material for MkDocs MIT/Insiders ayrı | https://github.com/mkdocs/mkdocs — https://www.mkdocs.org/ — https://squidfunk.github.io/mkdocs-material/ | | Wiki.js | AGPL-3.0 | https://github.com/requarks/wiki — https://js.wiki/ — https://docs.requarks.io/ | | TriliumNext | AGPL-3.0, kişisel bilgi tabanı | https://github.com/TriliumNext/Trilium | ### 3.2. Kriter matrisi (şartname: ücretsiz/OSS, self-hosted, Git, Markdown, AI-güncelleme, search, sidebar, kod, Mermaid, koyu tema, intranet) | Kriter | Docusaurus | MkDocs (+Material) | Wiki.js | TriliumNext | |---|---|---|---|---| | Ücretsiz/OSS | Evet (MIT) | Evet (BSD) | Evet (AGPL-3.0) | Evet (AGPL-3.0) | | Self-hosted intranet | Evet (statik dosya, Nginx) | Evet (statik dosya, Nginx) | Evet (Node.js + PostgreSQL/SQLite) | Kısmi (tek kullanıcılı sunucu modu; ekip intraneti için uygun değil) | | Git (sürümleme/diff/PR) | **Yerleşik: docs zaten Git'te** | **Yerleşik: docs zaten Git'te** | Harici: Git storage hedefi senkronu (ek yapılandırma) — https://docs.requarks.io/storage/git | Yerleşik değil (kendi sync formatı) | | Markdown | Evet (MDX dahil) | Evet | Evet (+WYSIWYG/HTML editör) — https://js.wiki/ | Evet (zengin not, wiki değil) | | AI ile güncelleme | **En kolay: düz `.md` dosyalar, ajan doğrudan düzenler** | **En kolay: düz `.md` dosyalar** | API/DB üzerinden (dolaylı) | API üzerinden (dolaylı) | | Arama | Yerleşik (lokal search eklentisi, offline çalışır) | Yerleşik (Material search, offline) | Yerleşik (DB tabanlı) | Yerleşik (kişisel) | | Sidebar/sürüm/i18n | Yerleşik (sidebar, versioning, i18n) | Eklenti/tema ile | Yerleşik navigasyon | Ağaç not yapısı | | Kod blokları | Prism, sekmeli kod, başlıklı blok | Highlight + ek eklentiler | Evet | Evet | | Mermaid | Resmi destek — https://docusaurus.io/docs/markdown-features/diagrams | Material ile destek — https://squidfunk.github.io/mkdocs-material/reference/diagrams/ | Modül ile (ek yapılandırma) | Sınırlı/eklenti | | Koyu tema | Yerleşik (kullanıcı değiştirebilir) | Yerleşik (Material toggle) | Yerleşik (light/dark) — https://js.wiki/ | Tema var | | Operasyonel yük | Derleme hattı (Node), DB yok | Derleme hattı (Python), DB yok | **Çalışan servis + DB (yedekleme/izleme ister)** | Düşük ama ekip akışına uygun değil | - Wiki.js özellik/editör/karanlık mod/DB seçenekleri: https://js.wiki/ - Docusaurus diyagram/search/versioning/i18n: https://docusaurus.io/docs/markdown-features/diagrams - MkDocs statik üretim: https://www.mkdocs.org/ **Free sürüm limit notları (docs):** Dört adayın da önerilen sürümü tam ücretsizdir — gizli kota yok: Docusaurus MIT (tüm özellikler açık), MkDocs BSD + Material Community (ücretli "Insider" eklentileri kapsam dışı, gerekli değil), Wiki.js AGPL-3.0 (tüm modüller dahil, Enterprise sürümü yok), TriliumNext AGPL-3.0. Elendikleri için değil, free-limitleri olmadığı için not düşüldü; seçim ölçütü Git-yerleşiklik + AI-güncelleme + sıfır servis yüküdür. ### 3.3. KARAR - **KARAR:** **Docusaurus** (repo'da `docs-site/` iskeleti; `docs/*.md` tek kaynak, site derlemesi CI'da). - **GEREKÇE:** Şartnamedeki "AI-güncelleme" kriterini yalnızca Git-yerleşik Markdown çözer (ajan `.md`'yi doğrudan yazar, review PR'dan geçer); Wiki.js'nin DB+servis yükü "basitlik" kuralını bozar ve Git akışını dolaylı hale getirir; TriliumNext tek-kullanıcı bilgi tabanıdır, 26 dosyalık ekip dokümantasyonu için yayın akışı yoktur; MkDocs'a karşı Docusaurus'u seçme nedeni: yerleşik sürümleme (LAB→PROD evrelerinde doküman sürümü), yerleşik i18n (TR öncelikli, EN yolu açık), MDX ile runbook içi parametreli bileşenler ve React ekosisteminde hazır intranet temaları — derleme maliyeti MkDocs ile eşdeğerdir. - **ALTERNATİF:** MkDocs + Material (eşdeğer sadelikte güçlü ikinci; Docusaurus derleme hattı sorun çıkarırsa geçiş maliyeti düşük çünkü kaynak yine düz Markdown). Wiki.js (DB yükü → elendi). TriliumNext (ekip akışı yok → elendi). - **RİSK:** Node derleme hattı → `docs-site/` kilitli bağımlılıklar + CI'da derleme testi; derleme bozulsa bile `docs/*.md` ham haliyle okunur (tek kaynak kaybı yok). - **MALİYET:** $0. - **LİSANS:** Docusaurus MIT; üretilen içerik bize ait. --- ## 4. Özet — üç tek-öneri 1. **Fleet:** Ansible CLI + Semaphore UI Community (Pro bağımlılığı yok — kanıt §1.2). 2. **Monitoring:** Prometheus + Node Exporter + Grafana OSS (17 metrik) + Uptime Kuma (UP/DOWN + bildirim). 3. **Docs:** Docusaurus (`docs/*.md` tek kaynak). ## 5. Açık sorular (karar günlüğüne taşınacak) - Semaphore Community'de lisansla dayatılan node kotası yoktur varsayımı `pricing` + repo davranışıyla doğrulandı; 700 node'lu gerçek yük testi LAB evresinde yapılır (Global Runner sayısı, eşzamanlılık). - Prometheus retention/kardinalite hesabı (700 × 60 sn scrape) LAB'da ölçülür, 14-MONITORING'e yazılır. - Uptime Kuma Push monitörleri için cihaz başına heartbeat betiği sözleşmesi 14-MONITORING'e yazılır. # Blueforce Linux Filo — 700 Saha Cihazı > %100 ücretsiz / self-hosted Linux filo mimarisi: başka bir DevOps mühendisinin sıfırdan kurabileceği 26 Türkçe dokümanın ana reposu. ## Amaç AdBlue saha otomasyonundaki ~700 Windows mini PC'yi, lisans maliyeti olmayan, merkezi yönetilebilir, kesintisiz çalışan bir Linux filosuna taşımak. Bu repo üç şeyi tek yerde toplar: 1. **Kararlar** — her teknik seçim için gerekçe, alternatif, risk, maliyet ve lisans notu. 2. **Kurulum ve işletme dokümanları** — `docs/` altında 26 Türkçe Markdown dosyası. 3. **Uygulanabilir iskelet** — `scripts/`, `ansible/`, `config/`, `monitoring/`, `rustdesk/` klasörleri (kod ve config Faz 3'te dolar). Öncelik sırası: **kesintisiz çalışma > veri kaybı yok > uzaktan erişim > bakım kolaylığı.** ## 700 Cihaz Özeti | Konu | Özet | |---|---| | Cihaz sayısı | ~700 saha PC'si (bayi/istasyon yanında) | | Cihaz kimliği | 8 haneli bayi no → Device ID `BF-`, hostname `bf-` (ör. `BF-12010193` / `bf-12010193`). Tüm sistemlerde aynı ID kullanılır. | | Hedef OS | Ubuntu LTS (sürüm Faz 1'de resmi kaynaktan doğrulanacak) | | Ağ | Turkcell modem arkası, CGNAT/NAT varsayımı; merkezle bağlantı WireGuard tüneli üzerinden | | Uzaktan erişim | RustDesk OSS self-hosted (hbbs/hbbr) ana yol; SSH + WireGuard yedek yol; RDP/GUI ihtiyaca göre | | Filo yönetimi | Ansible + Semaphore UI Community (aday; Faz 2'de karar) | | İzleme | Prometheus/Node Exporter/Grafana OSS veya Uptime Kuma (Faz 2'de karşılaştırılacak) | | Güncelleme politikası | **Onaysız update yasak** (apt, release upgrade, Docker image, MEG dahil). Rollout: LAB(2) → PILOT-1(5) → PILOT-2(20) → WAVE-1(50) → WAVE-2(100) → PRODUCTION | | Doküman dili | Türkçe; komut, dosya, servis ve terim adları İngilizce kalır | ## Docs Haritası (26 dosya) Tüm dokümanlar `docs/` altındadır ve `docs/_TEMPLATE.md` şablonuna uyar. | No | Dosya | Konu | |---|---|---| | 00 | `00-MASTER-PLAN.md` | Genel plan, fazlar, kapsam | | 01 | `01-ARCHITECTURE.md` | Uçtan uca mimari | | 02 | `02-DEVICE-NAMING-AND-INVENTORY.md` | `BF-` / `bf-` isimlendirme ve envanter | | 03 | `03-UBUNTU-BASELINE.md` | Ubuntu taban kurulum | | 04 | `04-GOLDEN-IMAGE-AND-PROVISIONING.md` | Golden image ve ilk kurulum | | 05 | `05-ONE-CLICK-INSTALLER.md` | Tek-tık kurucu | | 06 | `06-USERS-SSH-AND-PERMISSIONS.md` | Kullanıcılar, SSH, yetkiler | | 07 | `07-REMOTE-ACCESS.md` | RustDesk + SSH + RDP erişim yolları | | 08 | `08-WIREGUARD-AND-NETWORK.md` | WireGuard ve ağ | | 09 | `09-FLEET-MANAGEMENT.md` | Ansible/Semaphore filo yönetimi | | 10 | `10-UPDATE-AND-ROLLBACK-POLICY.md` | Güncelleme ve geri alma politikası | | 11 | `11-SECURITY-HARDENING.md` | Güvenlik sertleştirme | | 12 | `12-DOCKER-OPERATIONS.md` | Docker işletimi | | 13 | `13-MEG-LINUX-ACCEPTANCE.md` | MEG Linux kabul kriterleri | | 14 | `14-MONITORING-AND-HEALTH.md` | İzleme ve sağlık | | 15 | `15-LOGGING.md` | Log toplama ve saklama | | 16 | `16-POWER-LOSS-AND-AUTO-RECOVERY.md` | Elektrik kesintisi ve otomatik kurtarma | | 17 | `17-BACKUP-RECOVERY-AND-REINSTALL.md` | Yedekleme, kurtarma, yeniden kurulum | | 18 | `18-PILOT-AND-700-DEVICE-ROLLOUT.md` | Pilot ve 700 cihaz rollout | | 19 | `19-TROUBLESHOOTING.md` | Sorun giderme | | 20 | `20-FIELD-TECHNICIAN-RUNBOOK.md` | Saha teknisyeni runbook | | 21 | `21-CENTRAL-ADMIN-RUNBOOK.md` | Merkezi yönetici runbook | | 22 | `22-DOCUMENTATION-PLATFORM.md` | Doküman platformu (Docusaurus adayı) | | 23 | `23-SECURITY-AND-LICENSE-AUDIT.md` | Güvenlik ve lisans denetimi | | 24 | `24-DECISION-LOG.md` | Karar günlüğü (kaynak URL + sürüm + tarih ile) | | 25 | `25-IMPLEMENTATION-ROADMAP.md` | Uygulama yol haritası | Şablon: `docs/_TEMPLATE.md`. Karar günlüğü: `docs/24-DECISION-LOG.md`. ## Maliyet Kuralı - Hedef **%100 ücretsiz / self-hosted**'dir. - Ücretli veya lisans kısıtlı her araç dokümanda açıkça işaretlenir ve **ücretsiz alternatifi** ile birlikte verilir. - Varsayıma dayalı lisans kararı verilmez; resmi kaynak (resmi docs, GitHub, fiyat sayfası) URL + sürüm + tarih ile kaydedilir. - Örnek: AnyDesk kararı resmi kaynaktan doğrulanmadan yazılmaz. ## İsimlendirme Örneği Bayi no `12010193` için: - Device ID: `BF-12010193` - Hostname: `bf-12010193` - WireGuard peer adı, RustDesk ID etiketi, Ansible inventory adı, monitoring etiketi: hepsi aynı ID'yi kullanır. ## Klasör Yapısı ``` . ├── README.md ├── SUMMARY.md # Faz 4'te üretilecek ├── docs/ # 26 doküman (00–25) + _TEMPLATE.md ├── scripts/ │ ├── install/ # ilk kurulum scriptleri (Faz 3'te sözleşme/imza) │ ├── maintenance/ # bakım scriptleri │ ├── diagnostics/ # tanı scriptleri │ └── recovery/ # kurtarma scriptleri ├── ansible/ │ ├── inventory/ │ ├── playbooks/ │ └── roles/ ├── config/ │ ├── ssh/ │ ├── wireguard/ │ ├── firewall/ │ ├── docker/ │ └── systemd/ ├── monitoring/ # izleme config ve panoları ├── rustdesk/ # hbbs/hbbr self-hosted config ├── docs-site/ # doküman sitesi iskeleti (Docusaurus adayı) ├── tests/ # doğrulama testleri └── research/ # Faz 1 kaynak doğrulama notları ``` ## Durum - [x] Faz 0: repo iskeleti + şablon - [ ] Faz 1: kaynak doğrulama (resmi docs/GitHub, 10 kritik konu) - [ ] Faz 2: mimari kararlar - [ ] Faz 3: 26 dokümanın parti parti yazımı - [ ] Faz 4: QA, tutarlılık denetimi, SUMMARY.md # Blueforce Linux — Yönetici Özeti (SUMMARY) > 700 saha cihazlık filonun kesin kararları, en büyük 10 operasyonel riski ve ücretsiz-bileşen listesi. > Ayrıntı `docs/` dosyalarındadır; her cevap ilgili dokümana linklenir. Kararların kilit kaynağı > [`docs/24-DECISION-LOG.md`](docs/24-DECISION-LOG.md) (K-01…K-14), izlenebilirlik tablosu Ek B'dedir (S-01…S-27). > Mimari tek bakış: [`docs/01-ARCHITECTURE.md`](docs/01-ARCHITECTURE.md). Commit yok, FREE sürümler baz. --- ## 1. Şartnamedeki 27 Sorunun Kesin Cevabı 1. **Hangi Ubuntu? Server mı Desktop mı?** — Ubuntu **Server 26.04.1+ LTS**, GUI'siz minimal kurulum. Desktop ISO'sunun 6 GB RAM şartı saha bütçesini zorlar; GUI sonradan eklenir. → [`03`](docs/03-UBUNTU-BASELINE.md) (K-01) 2. **GNOME mu XFCE mi?** — Cevap **GNOME** (Ubuntu Desktop standardı, kullanıcı kararı); XFCE elendi. → [`03`](docs/03-UBUNTU-BASELINE.md) (K-02) 3. **Normal boot GUI mi terminal mi?** — Cihazlar **terminale** (`multi-user.target`) boot eder; kapalı GUI = düşük RAM, düşük saldırı yüzeyi, öngörülebilir boot. → [`03`](docs/03-UBUNTU-BASELINE.md), [`16`](docs/16-POWER-LOSS-AND-AUTO-RECOVERY.md) (K-03) 4. **GUI nasıl açılıp kapatılacak?** — `sudo bf-gui-on` / `sudo bf-gui-off` ile systemd üzerinden (display-manager + `xrdp`/`xrdp-sesman`); varsayılan kapalıdır. → [`03`](docs/03-UBUNTU-BASELINE.md), [`07`](docs/07-REMOTE-ACCESS.md) (K-03) 5. **Cihaz isimlendirme standardı?** — 8 haneli bayi no → insan arayüzünde `BF-` (büyük), makine adında `bf-` (küçük); 9 sistemde (hostname, WireGuard peer, RustDesk, MeshCentral, Ansible, Prometheus, log, envanter, runbook) join anahtarıdır. Örn. `BF-12010193` / `bf-12010193`. → [`02`](docs/02-DEVICE-NAMING-AND-INVENTORY.md) (K-12) 6. **İlk PC hazırlığı nasıl?** — İmaj dondurulmadan **önce** `bf-hardware-inventory.sh` ile donanım taraması (CPU/RAM/disk/NIC/GPU profilleri); profilsiz imaj sahada rastgele kırılır. → [`18`](docs/18-PILOT-AND-700-DEVICE-ROLLOUT.md), [`02`](docs/02-DEVICE-NAMING-AND-INVENTORY.md) (K-09) 7. **Yeni PC kurulumu kaç adım?** — Teknisyen için **4 adım**: USB tak → Ubuntu kur → `blueforce-install.sh` + 8 haneli bayi no → `bf-status` ile READY gör, çık (<15 dk aktif iş). → [`20`](docs/20-FIELD-TECHNICIAN-RUNBOOK.md), [`04`](docs/04-GOLDEN-IMAGE-AND-PROVISIONING.md) (K-09) 8. **Installer nasıl çalışacak?** — Tek giriş `blueforce-install.sh --dealer-id <8hane>`; **18 idempotent modül** (precheck → dealer-id → paketler → GNOME → xRDP → SSH → WireGuard → Docker → MEG → RustDesk → MeshCentral → monitoring → firewall → envanter → logging → boot → bootstrap → final-check), çıkış kodu 0 = READY, format kapısı + onay ekranlı. → [`05`](docs/05-ONE-CLICK-INSTALLER.md) (K-09) 9. **WireGuard mimarisi?** — **Hub-spoke**: merkez hub (`10.8.0.1`), her cihaz spoke (peer adı `bf-`, sabit `/32`); istemcide `PersistentKeepalive = 25` + `wg-quick@wg0` enable; SSH/RDP yalnızca tünel içinden, dış dünyaya kapalı. → [`08`](docs/08-WIREGUARD-AND-NETWORK.md) (K-05) 10. **SSH güvenliği nasıl?** — `blueforce` bakım kullanıcısı + **anahtar zorunlu**, password auth kapalı, root SSH kapalı; 700 cihaza tek private key **yayılmaz** — admin başına anahtar, Ansible ile merkezi `authorized_keys` dağıtımı, uzun vadede SSH CA hedefi. → [`06`](docs/06-USERS-SSH-AND-PERMISSIONS.md) (K-13) 11. **RDP nasıl?** — **xRDP + GNOME**, yalnızca WireGuard üzerinden; internete açık RDP portu yoktur. Grafik katman kapalıyken RDP açılmaz (`bf-gui-on` gerekir). → [`07`](docs/07-REMOTE-ACCESS.md) (K-03/K-05) 12. **RustDesk self-host neyi kapsar?** — **RustDesk OSS** istemci + `hbbs`/`hbbr` kendi VDS'imizde; sınırsız istemci, E2E şifreli oturum, dosya transferi, katılımsız erişim — hepsi ücretsiz. İstemci VDS genel adresine doğrudan çıktığı için **WireGuard çökse bile** kanal ayaktadır. → [`07`](docs/07-REMOTE-ACCESS.md) (K-04) 13. **AnyDesk ücretsiz kullanılabilir mi?** — **HAYIR.** Free sürüm yalnızca kişisel kullanımı kapsar; 700 kurumsal cihaz ticari kullanımdır, lisanssız kullanım sözleşme ihlalidir. Mimari dışıdır; yalnızca müşteride zaten ticari lisans varsa opsiyonel `--with-anydesk` modülü. → [`07`](docs/07-REMOTE-ACCESS.md), [`23`](docs/23-SECURITY-AND-LICENSE-AUDIT.md) (K-04) 14. **4. erişim kanalı ne?** — **MeshCentral** (self-hosted, Apache-2.0): ikincil yönetim düzlemi (terminal/dosya/envanter), RustDesk gibi WireGuard-bağımsız. 4 kanal = SSH-over-WG, xRDP, RustDesk, MeshCentral. → [`07`](docs/07-REMOTE-ACCESS.md) (K-04) 15. **Fleet yönetimi ne?** — Birincil **Ansible CLI** (SSH, agent'sız); **15 standart işlem** (`bf-ping`, `bf-status`, `bf-gui-on/off`, `reboot`…) playbook + Semaphore task şablonu olarak tanımlı; serbest komut yalnızca acil prosedürle; hedefleme tek → grup → pilot → tüm-filo. → [`09`](docs/09-FLEET-MANAGEMENT.md) (K-06) 16. **Semaphore kullanmalı mıyız?** — Evet, **Semaphore UI Community** (self-hosted, MIT, $0) operatör arayüzü olarak; Semaphore çökse Ansible CLI bağımsız çalışır. **Pro gerekmez** (zaten 500 node sınırıyla 700 cihaza yetmez). Community'de OIDC/2FA/Vault yoktur → yalnızca WireGuard arkasından erişim. → [`09`](docs/09-FLEET-MANAGEMENT.md) (K-06) 17. **Monitoring ne?** — Birincil **Prometheus + Node Exporter + Grafana OSS** (17 metriğin tamamı, özel metrikler textfile `.prom` ile); tamamlayıcı **Uptime Kuma** (teknisyen dostu UP/DOWN + Push "last seen" + bildirim). Netdata Cloud Free 5 node kotasıyla elendi. → [`14`](docs/14-MONITORING-AND-HEALTH.md) (K-07) 18. **Update onayı nasıl, kim başlatır, başarısız olursa ne olur?** — **Onaysız update yasaktır.** Başlatma yetkisi yalnızca **merkezi yöneticide** (asıl + vekil); her dalga Semaphore onay kapısından geçer: LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD(~523). Başarısız dalga bir sonrakine geçmez, yayılım durur, etkilenmiş dalga rollback zinciriyle geri alınır; acil yamalar da aynı zincirden (kısaltılmış bekleme, kapılar kalkmaz) geçer. → [`10`](docs/10-UPDATE-AND-ROLLBACK-POLICY.md), [`21`](docs/21-CENTRAL-ADMIN-RUNBOOK.md) (K-11) 19. **`unattended-upgrades` ne olacak?** — Golden image'da **kapalı** (`20auto-upgrades` değerleri `0` + `apt-daily*.timer` maskeleme); kapatılmazsa Ubuntu varsayılanı 700 cihazı ilk açılışta güncellemeye kalkar. Kilit, kurucu final-check + monitoring metriğiyle denetlenir. → [`03`](docs/03-UBUNTU-BASELINE.md), [`10`](docs/10-UPDATE-AND-ROLLBACK-POLICY.md) (K-11) 20. **Docker politikası ne?** — Restart `unless-stopped`; **`latest` yasak** (sabit tag, digest P2'de değerlendirilir); **Watchtower yasak**; log rotasyonu `daemon.json`'da zorunlu; yayımlanan portlar UFW bypass'a karşı **`127.0.0.1`**'e bağlanır. → [`12`](docs/12-DOCKER-OPERATIONS.md), [`11`](docs/11-SECURITY-HARDENING.md) (K-10) 21. **Log disiplini + disk dolması engeli?** — journald kalıcı + tavanlı (`SystemMaxUse`/`MaxFileSec`), Docker `json-file` rotasyonlu, 7 kaynak standardı + `bf-status`/`bf-diagnostics`/`bf-support-bundle` (secret sızdırmaz); merkezi Loki/ELK ilk fazda yok (Faz-sonrası hedef). Rotasyonsuz log = kilitlenen cihaz kuralıyla disk taşması mimari düzeyde engellenir. → [`15`](docs/15-LOGGING.md), [`12`](docs/12-DOCKER-OPERATIONS.md) (K-14/K-10) 22. **Elektrik kesintisi ve dönüşü?** — BIOS `Restore on AC Power Loss = Power On` ile cihaz kendiliğinden açılır; systemd sıralı boot zinciri (network → `wg-quick` → docker → MEG → agent'lar) + `unless-stopped` ile her katman kendiliğinden toparlanır, ek kurtarma servisi yoktur. → [`16`](docs/16-POWER-LOSS-AND-AUTO-RECOVERY.md) (K-03/K-10) 23. **İnternet paketi bitimi ve dönüşü?** — Kota bitince tünel sessizce ölür (arıza değil, "çevrimdışı" görünür; Kuma "last seen" alarmı + bayiye bildirim); kota dönüşünde `wg-quick` + keepalive **kendiliğinden** yeniden handshake eder, ek işlem gerekmez. Kota-dostu keepalive (25 sn, PİLOT verisiyle ayarlanır). → [`08`](docs/08-WIREGUARD-AND-NETWORK.md) (K-05) 24. **PC bozulunca teknisyen ne yapar?** — **9 seviyeli kurtarma merdiveni** (L1 servis restart → … → L8 USB ile sıfırdan imaj → L9 merkez inşası); cihaz "değiştirilebilir" sayılır, kimlik + config merkezden yeniden basılır, cihaz-başı veri yedeği yoktur. Çoğu arıza 4 kanaldan biriyle uzaktan çözülür, sahaya gidilmez. → [`17`](docs/17-BACKUP-RECOVERY-AND-REINSTALL.md), [`20`](docs/20-FIELD-TECHNICIAN-RUNBOOK.md) 25. **Golden image yöntemi ne?** — Kimliksiz zemin (Server 26.04.1+ minimal + update kapalı + kurucu) USB/autoinstall ile kurulur; cihaz-spesifik kimlik (`bf-`) imajda **değil ilk açılışta** enjekte edilir. Clonezilla bit-kopya yalnızca yedek yöntemdir (donanım-fragil). → [`04`](docs/04-GOLDEN-IMAGE-AND-PROVISIONING.md), [`05`](docs/05-ONE-CLICK-INSTALLER.md) (K-09) 26. **700 cihaz rollout nasıl?** — Üstel dalgalar: LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD(~523); hiçbir dalga önceki çıkış kriterini sağlamadan başlamaz; halt kuralları (≥2 kritik arıza / >%10 çevrimdışı / 2× kurulum süresi) yayılımı durdurur. → [`18`](docs/18-PILOT-AND-700-DEVICE-ROLLOUT.md) (K-11) 27. **Docs platformu ne?** — **Docusaurus**; kaynak `docs/*.md` tek-kaynaktır, site CI'da derlenir, siteye elle yazma yasaktır. Yedek: MkDocs + Material. → [`22`](docs/22-DOCUMENTATION-PLATFORM.md) (K-08) --- ## 2. 700 İstasyonda 5 Yıllık En Büyük 10 Operasyonel Risk | # | Risk | Önlem | |---|---|---| | 1 | **Onaysız update kaçağı** — tek imajda `unattended-upgrades` açık unutulur, 700 cihaz kendiliğinden güncellenir. | Kurucu `18-final-check` kapısı + monitoring'de `unattended_upgrade_active` metriği (1 = ihlal alarmı); K-11 kilidi üç dosyada (03/10/12) denetlenir. | | 2 | **Tek VDS ortak arıza noktası** — hub + hbbs/hbbr + MeshCentral aynı makinede; VDS giderse 2 bağımsız kanal da gider. | Yedek VDS planı + üç ayda bir "boş VDS'e sıfırdan kur" L9 tatbikatı; IaC Git'te, veriler günlük dosya yedeğinde ([`17`](docs/17-BACKUP-RECOVERY-AND-REINSTALL.md)). | | 3 | **Heterojen/yeni-parti donanım imajı kırar** — 5 yılda donanım partileri değişir, sürücü eksiği sahada patlar. | Her yeni parti LAB profil matrisine girer; imaj, envanter taraması olmadan dondurulmaz ([`18`](docs/18-PILOT-AND-700-DEVICE-ROLLOUT.md), [`04`](docs/04-GOLDEN-IMAGE-AND-PROVISIONING.md)). | | 4 | **Semaphore kapı bypass'ı** — acil bahanesiyle CLI'dan onaysız PROD run, dalga disiplini kağıtta kalır. | Semaphore dışı PROD run'ı yasak + denetim logu izlenir; onaysız run testi merkez test planında ([`21`](docs/21-CENTRAL-ADMIN-RUNBOOK.md)). | | 5 | **Prometheus kardinalite/retention şişmesi** — 700 node × 5 yıl metrik ve log hacmi merkez diskini ve sorguları yorar. | 60 sn scrape, 30–90 gün retention, `BF-` etiket disiplini; PİLOT hacim ölçümüyle donanım siparişi, federasyon hedefi ([`14`](docs/14-MONITORING-AND-HEALTH.md)). | | 6 | **Teknisyen hatası ölçeklenir** — yanlış bayi no (kimlik çakışması), READY görülmeden çıkış, eski USB ile kurulum. | Kurucuda format kapısı + sesli onay ekranı; kontrol listesi imzalanmadan iş kapanmaz; USB sürüm etiketi ([`20`](docs/20-FIELD-TECHNICIAN-RUNBOOK.md), [`02`](docs/02-DEVICE-NAMING-AND-INVENTORY.md)). | | 7 | **Turkcell hat/kota davranış değişimi** — CGNAT eşleme süresi veya kota politikası değişir, tüneller toplu düşer. | P1 7/24 kopma sayacıyla keepalive saha verisine bağlanır; Kuma "last seen" + paket-bitim bildirim akışı ([`08`](docs/08-WIREGUARD-AND-NETWORK.md)). | | 8 | **Upstream lisans/kapsam daralması** — RustDesk/Grafana/Semaphore free kapsamı 5 yıl içinde değişebilir. | Yıllık lisans gözden geçirme + her ücretli-adayın ücretsiz karşılığı tabloda hazır; değişiklik 24-PR ile işletilir ([`23`](docs/23-SECURITY-AND-LICENSE-AUDIT.md)). | | 9 | **SSH anahtar sızıntısı** — tek private key 700 cihazı açar; ayrılan personelin anahtarı unutulur. | Per-admin anahtar + Ansible merkezi dağıtım/iptal, paylaşılan key politikada yasak + denetim scripti; SSH CA Faz-sonrası hedef ([`06`](docs/06-USERS-SSH-AND-PERMISSIONS.md)). | | 10 | **IaC drift + denenmemiş kurtarma** — Git'teki playbook gerçek merkezden sapar, L9 günü çalışmaz. | Üç aylık L9 tatbikatı + aylık yedek-doğrulama job'ı (açılabilirlik + hash); sonuç monitoring'e işlenir ([`17`](docs/17-BACKUP-RECOVERY-AND-REINSTALL.md)). | --- ## 3. Gerçekten Tamamen Ücretsiz Olanlar (FREE katman, lisans bedeli $0) > Kapsam: yazılım lisansları. Merkezi VDS kirası altyapı maliyetidir, lisans değildir. | Bileşen | Sürüm/katman | Lisans | Kaynak | |---|---|---|---| | Ubuntu Server LTS | 26.04.1+ (ESM'e bel bağlanmaz) | Açık kaynak | [`03`](docs/03-UBUNTU-BASELINE.md) | | GNOME | saha standardı (Ubuntu Desktop) | GPL/LGPL bileşenler | [`03`](docs/03-UBUNTU-BASELINE.md) | | xRDP | v0.10.6.1 | Apache-2.0 | [`03`](docs/03-UBUNTU-BASELINE.md) | | WireGuard + wg-quick | wireguard-tools 1.0.20250521-1ubuntu1 | OSS | [`08`](docs/08-WIREGUARD-AND-NETWORK.md) | | OpenSSH / UFW | sistem paketleri | BSD / GPL bileşenler | [`06`](docs/06-USERS-SSH-AND-PERMISSIONS.md), [`11`](docs/11-SECURITY-HARDENING.md) | | Docker CE | (Business/Scout mimaride yok) | Ücretsiz | [`12`](docs/12-DOCKER-OPERATIONS.md) | | RustDesk istemci + sunucu (hbbs/hbbr) | 1.4.9 / 1.1.16, OSS | AGPL-3.0 | [`07`](docs/07-REMOTE-ACCESS.md) | | MeshCentral | 1.2.5 | Apache-2.0 | [`07`](docs/07-REMOTE-ACCESS.md) | | Ansible | CLI, agent'sız | GPL-3.0 | [`09`](docs/09-FLEET-MANAGEMENT.md) | | Semaphore UI | Community ($0, free forever) | MIT | [`09`](docs/09-FLEET-MANAGEMENT.md) | | Prometheus + Node Exporter | kotasız self-host | Apache-2.0 | [`14`](docs/14-MONITORING-AND-HEALTH.md) | | Grafana | OSS (değişikliksiz self-host) | AGPL-3.0 | [`14`](docs/14-MONITORING-AND-HEALTH.md) | | Uptime Kuma | tek konteyner | MIT | [`14`](docs/14-MONITORING-AND-HEALTH.md) | | Docusaurus | statik site | MIT | [`22`](docs/22-DOCUMENTATION-PLATFORM.md) | | systemd / journald | log + servis zinciri | LGPL bileşenler | [`15`](docs/15-LOGGING.md), [`16`](docs/16-POWER-LOSS-AND-AUTO-RECOVERY.md) | **Ücretsiz DEĞİL / mimaride YOK (kullanılırsa lisans gerekir):** AnyDesk Free (ticari kullanımda ihlal) → [`23`](docs/23-SECURITY-AND-LICENSE-AUDIT.md); RustDesk Pro (gerekmez); Semaphore Pro/Enterprise (500 node sınırı + ücretli); Netdata Cloud Free (5 node kotası); Tailscale/ZeroTier ücretli kademeler; Ubuntu Pro/ESM (bel bağlanmaz). Tam hüküm tablosu [`23 §9`](docs/23-SECURITY-AND-LICENSE-AUDIT.md), kaynak URL'ler [`24 Ek A`](docs/24-DECISION-LOG.md). --- ## Ek: QA Doğrulama (2026-09-15) - **26 dosya mevcut:** `docs/00` … `docs/25` tamamı var (+ `_TEMPLATE.md`). - **13 başlık:** 26 dosyanın tamamında §1–§13 sıralı ve eksiksiz. - **Mermaid:** 26/26 dosyada ≥1 diyagram (03/19/21/23/24'e bu özetle birlikte eklendi). - **BF-/bf- tutarlılığı:** insan arayüzü `BF-`, makine adı `bf-`; aykırı kullanım yok. - **Ücretli sızıntı:** yok — ücretli geçen her satır elendi/hüküm/denetim bağlamında. - **Onaysız-update kapısı:** yok — `unattended-upgrades`/`latest`/Watchtower yasakları 03/10/12'de ve K-11'de kilitli; acil yama bile onay + rollback testinden geçer. .directory .hermes/