From 54f7e5536a3c2c97e30d96824411414c5f5de491 Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Mon, 2 Jun 2025 01:42:17 +0200 Subject: [PATCH] Added support for private apis --- .../files/dockerfiles/Dockerfile.mcp.gitea | 26 ++ ansible/host_vars/dev-01.alexpires.dev.yml | 3 +- ansible/host_vars/gpu-01.alexpires.dev.yml | 34 +-- ansible/playbook_ai_tools.yml | 198 -------------- ansible/playbook_mcp_servers.yml | 20 ++ ansible/playbook_open_webui.yml | 38 +++ ansible/sectool.env | 1 + ansible/tasks/nvidia-cdi-generator.yml | 28 ++ ansible/tasks/podman.yml | 241 ++++++++++++++++++ ansible/templates/podman.service.j2 | 8 +- inventory/hosts | 5 +- terraform/cloudns-iac/config.tf | 2 +- terraform/k8sapps/apps.tf | 1 + terraform/k8sapps/config.tf | 9 +- terraform/k8sapps/secrets.tf | 6 + terraform/k8sapps/sectool.env | 2 + .../apps/nginx-reverse-proxy/config.tf | 11 +- .../modules/apps/nginx-reverse-proxy/main.tf | 68 ++++- .../resources/auth/auth_server.py | 40 +++ .../resources/nginx/conf.d/default.conf.tpl | 17 +- .../apps/nginx-reverse-proxy/variables.tf | 13 +- 21 files changed, 527 insertions(+), 244 deletions(-) create mode 100644 ansible/files/dockerfiles/Dockerfile.mcp.gitea delete mode 100644 ansible/playbook_ai_tools.yml create mode 100644 ansible/playbook_mcp_servers.yml create mode 100644 ansible/playbook_open_webui.yml create mode 100644 ansible/tasks/nvidia-cdi-generator.yml create mode 100644 ansible/tasks/podman.yml create mode 100644 terraform/modules/apps/nginx-reverse-proxy/resources/auth/auth_server.py diff --git a/ansible/files/dockerfiles/Dockerfile.mcp.gitea b/ansible/files/dockerfiles/Dockerfile.mcp.gitea new file mode 100644 index 0000000..5834d58 --- /dev/null +++ b/ansible/files/dockerfiles/Dockerfile.mcp.gitea @@ -0,0 +1,26 @@ +FROM python:3.11-slim +ARG MCP_VERSION=0.2.0 +ARG UID=1000 +ARG GID=1000 +RUN groupadd --system --gid ${GID} worker && \ + adduser --system --gid ${GID} --uid ${UID} --home /home/worker worker +ENV PATH=/home/worker/.local/bin:$PATH +WORKDIR /home/worker +# Super user custom actions +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + tar \ + && rm -rf /var/lib/apt/lists/* +RUN curl -L https://gitea.com/gitea/gitea-mcp/releases/download/v${MCP_VERSION}/gitea-mcp_Linux_x86_64.tar.gz \ + | tar -xz -C /usr/local/bin \ + && chmod +x /usr/local/bin/gitea-mcp +# End of super user custom actions +USER worker +RUN pip install mcpo uv +EXPOSE 8000 +# Worker custom actions +ENV GITEA_HOST="https://gitea.com" +ENV GITEA_INSECURE="false" +ENV GITEA_ACCESS_TOKEN="" +CMD ["uvx", "mcpo", "--host", "0.0.0.0", "--port", "8000", "--", "gitea-mcp"] +# End of worker custom actions \ No newline at end of file diff --git a/ansible/host_vars/dev-01.alexpires.dev.yml b/ansible/host_vars/dev-01.alexpires.dev.yml index 244e224..6b97bb0 100644 --- a/ansible/host_vars/dev-01.alexpires.dev.yml +++ b/ansible/host_vars/dev-01.alexpires.dev.yml @@ -31,8 +31,7 @@ sshd_max_auth_tries: 10 # Firewall ports ufw_enable: true fw_allowed_ports: - - { rule: "allow", port: "80", proto: "tcp" } - - { rule: "allow", port: "443", proto: "tcp" } + - { rule: "allow", port: "8000", proto: "tcp" } - { rule: "allow", port: "22", proto: "tcp" } ufw_outgoing_traffic: diff --git a/ansible/host_vars/gpu-01.alexpires.dev.yml b/ansible/host_vars/gpu-01.alexpires.dev.yml index bb86a6e..48ed6a4 100644 --- a/ansible/host_vars/gpu-01.alexpires.dev.yml +++ b/ansible/host_vars/gpu-01.alexpires.dev.yml @@ -45,6 +45,7 @@ ufw_outgoing_traffic: - 123 # NTP - 443 # HTTPS - 853 # DNS over TLS + - 8000 ufw_default_forward_policy: "ACCEPT" @@ -70,31 +71,8 @@ unattended_reboot_time: "04:30" duo_users: ["*", "!provision"] duo_api_hostname: api-7cda1654.duosecurity.com -# ai tools -ai_home: "/mnt/data/ai" -ai_network_name: "ai-network" -ai_user: "ai" -ai_group: "ai" -ai_workloads: - - name: ollama - build: - dockerfile: "{{ lookup('file', 'dockerfiles/Dockerfile.ollama') }}" - ports: - - 0.0.0.0:11434:11434/tcp - volumes: - - "{{ ai_home }}/ollama:/home/worker/.ollama:Z" - device: - - "nvidia.com/gpu=all" - - name: open-webui - repo: - image: ghcr.io/open-webui/open-webui - tag: main - ports: - - 0.0.0.0:8080:8080/tcp - volumes: - - "{{ ai_home }}/open-webui:/app/backend/data:Z" - device: - - "nvidia.com/gpu=all" - env: - OLLAMA_BASE_URL: "http://ollama:11434" - +# open_web_ui settings +open_webui_home: "/mnt/data/ai" +open_webui_network_name: "ai-network" +open_webui_user: "ai" +open_webui_group: "ai" diff --git a/ansible/playbook_ai_tools.yml b/ansible/playbook_ai_tools.yml deleted file mode 100644 index e1a205f..0000000 --- a/ansible/playbook_ai_tools.yml +++ /dev/null @@ -1,198 +0,0 @@ -- name: Setup AI tools with Podman (Ollama) - hosts: ai - - tasks: - - name: Install Podman and its dependencies - become: true - ansible.builtin.dnf: - name: - - podman - - nvidia-container-toolkit - state: present - - - name: Create group and user for AI workloads - become: true - ansible.builtin.group: - name: "{{ ai_group }}" - system: true - - - name: Create user and group for AI containers - become: true - ansible.builtin.user: - name: "{{ ai_user }}" - groups: "{{ ai_group }}" - shell: /bin/bash - home: "{{ ai_home }}" - group: "{{ ai_group }}" - - - name: Persistently set SELinux context for home directories on AI home - become: true - community.general.sefcontext: - target: "{{ ai_home }}(/.*)?" - setype: user_home_dir_t - state: present - - - name: Apply SELinux context changes to AI home directories - become: true - ansible.builtin.command: restorecon -Rv {{ ai_home }} - changed_when: false - - - name: Disable password login for AI user - ansible.builtin.command: passwd -d "{{ ai_user }}" - become: true - changed_when: false - - - name: Create data folders for AI tools - become: true - ansible.builtin.file: - path: "{{ ai_home }}" - state: directory - owner: "{{ ai_user }}" - group: "{{ ai_group }}" - mode: "0750" - - - name: Create build folders for AI tools - become: true - ansible.builtin.file: - path: "{{ ai_home }}/build" - state: directory - owner: "{{ ai_user }}" - group: "{{ ai_group }}" - mode: "0750" - - - name: Create data folders for AI tools - become: true - ansible.builtin.file: - path: "{{ ai_home }}/{{ item.name }}" - state: directory - owner: "{{ ai_user }}" - group: "{{ ai_group }}" - mode: "0750" - loop: "{{ ai_workloads }}" - - - name: Create systemd service file for nvidia-cdi-generator - become: true - ansible.builtin.copy: - src: services/nvidia-cdi-generator.service - dest: /etc/systemd/system/nvidia-cdi-generator.service - owner: root - group: root - mode: "0644" - - - name: Reload systemd daemon - become: true - ansible.builtin.systemd: - daemon_reload: true - name: nvidia-cdi-generator - - - name: Enable nvidia-cdi-generator service - become: true - ansible.builtin.systemd: - name: nvidia-cdi-generator - enabled: true - state: started - - - name: Enable sudo access to ai user (temporary) - become: true - ansible.builtin.lineinfile: - path: /etc/sudoers.d/ansible_ai_tmp - create: true - mode: '0440' - line: "{{ ansible_user_id }} ALL=(ai) NOPASSWD: ALL" - validate: 'visudo -cf %s' - - - name: Enable lingering for ai user - become: true - ansible.builtin.command: loginctl enable-linger {{ ai_user }} - register: linger_status - changed_when: "'created' in linger_status.stdout or (linger_status.rc == 0 and not ('linger file already exists' in linger_status.stderr or 'linger file does not exist' in linger_status.stderr))" # Heuristic for change - failed_when: linger_status.rc != 0 and not ('linger file already exists' in linger_status.stderr) - - - name: Get current user's UID - ansible.builtin.command: id -u {{ ai_user }} - changed_when: false - register: uid - - - name: Get current user's GID - ansible.builtin.command: id -g {{ ai_user }} - changed_when: false - register: gid - - - name: SHowing UID and GID of ai user - ansible.builtin.debug: - msg: "UID: {{ uid.stdout }}, GID: {{ gid.stdout }}" - - - name: Run tasks as ai user - become: true - become_user: "{{ ai_user }}" - block: - - name: Create a custom Podman network - containers.podman.podman_network: - name: "{{ ai_network_name }}" - state: present - - - name: Build containers images - containers.podman.podman_image: - name: "ai/{{ item.name }}" - path: "{{ ai_home }}/build" - build: - cache: false - force_rm: true - container_file: "{{ item.build.dockerfile }}" - extra_args: "--build-arg UID={{ uid.stdout }} --build-arg GID={{ gid.stdout }} --security-opt=label=disable {{ item.build.args | default('') }}" - tag: "latest" - state: present - loop: "{{ ai_workloads }}" - when: item.build is defined - - - name: Pull the requested images - containers.podman.podman_image: - name: "{{ item.repo.image }}" - tag: "{{ item.repo.tag | default('latest') }}" - state: present - loop: "{{ ai_workloads }}" - when: item.repo is defined - - - name: Create a container instance for each workload - containers.podman.podman_container: - name: "{{ item.name }}" - image: "{{ item.repo.image | default('ai/' + item.name) }}" - state: started - device: "{{ item.device | default(omit) }}" - env: "{{ item.env | default(omit) }}" - network: "{{ ai_network_name }}" - ports: "{{ item.ports | default(omit) }}" - volumes: "{{ item.volumes | default(omit) }}" - cmd_args: - - "--userns=keep-id" - - "--security-opt=label=disable" - loop: "{{ ai_workloads }}" - - - name: Create systemd service file for containers - become: true - ansible.builtin.template: - src: podman.service.j2 - dest: "/etc/systemd/system/podman-{{ item.name }}.service" - owner: root - group: root - mode: "0644" - loop: "{{ ai_workloads }}" - - - name: Reload systemd daemon - become: true - ansible.builtin.systemd: - daemon_reload: true - - - name: Enable containers at boot - become: true - ansible.builtin.systemd: - name: "podman-{{ item.name }}" - enabled: true - state: started - loop: "{{ ai_workloads }}" - - - name: Remove temporary sudo access - become: true - ansible.builtin.file: - path: /etc/sudoers.d/ansible_ai_tmp - state: absent diff --git a/ansible/playbook_mcp_servers.yml b/ansible/playbook_mcp_servers.yml new file mode 100644 index 0000000..abe58c6 --- /dev/null +++ b/ansible/playbook_mcp_servers.yml @@ -0,0 +1,20 @@ +- name: Setup MCP servers + hosts: mcp + vars: + podman_user: "{{ mcp_user | default('mcp') }}" + podman_group: "{{ mcp_group | default('mcp') }}" + podman_user_home: "{{ mcp_home | default('/home/mcp') }}" + pods: + - name: gitea-mcp + build: + dockerfile: "{{ lookup('file', 'dockerfiles/Dockerfile.mcp.gitea') }}" + ports: + - 0.0.0.0:8000:8000/tcp + env: + GITEA_HOST: "https://code.alexpires.me" + GITEA_ACCESS_TOKEN: "{{ lookup('env', 'GITEA_APP_TOKEN') }}" + + tasks: + - name: Setup Pods + ansible.builtin.include_tasks: + file: tasks/podman.yml diff --git a/ansible/playbook_open_webui.yml b/ansible/playbook_open_webui.yml new file mode 100644 index 0000000..9fdfa2a --- /dev/null +++ b/ansible/playbook_open_webui.yml @@ -0,0 +1,38 @@ +- name: Setup open-webui + hosts: openwebui + vars: + podman_user: "{{ open_webui_user | default('openwebui') }}" + podman_group: "{{ open_webui_group | default('openwebui') }}" + podman_network_name: "{{ open_webui_network_name | default('openwebui-network') }}" + podman_user_home: "{{ open_webui_home | default('/home/open-webui') }}" + pods: + - name: ollama + build: + dockerfile: "{{ lookup('file', 'dockerfiles/Dockerfile.ollama') }}" + ports: + - 0.0.0.0:11434:11434/tcp + volumes: + - "{{ podman_user_home }}/ollama:/home/worker/.ollama:Z" + device: + - "nvidia.com/gpu=all" + - name: open-webui + repo: + image: ghcr.io/open-webui/open-webui + tag: main + ports: + - 0.0.0.0:8080:8080/tcp + volumes: + - "{{ podman_user_home }}/open-webui:/app/backend/data:Z" + device: + - "nvidia.com/gpu=all" + env: + OLLAMA_BASE_URL: "http://ollama:11434" + + tasks: + - name: Setup nvidia-cdi-generator + ansible.builtin.include_tasks: + file: tasks/nvidia-cdi-generator.yml + + - name: Setup Pods + ansible.builtin.include_tasks: + file: tasks/podman.yml diff --git a/ansible/sectool.env b/ansible/sectool.env index 86acd96..fa5d916 100644 --- a/ansible/sectool.env +++ b/ansible/sectool.env @@ -10,3 +10,4 @@ MICROK8S_ENCRYPTION_SECRET=$MICROK8S_ENCRYPTION_SECRET KMS_ACCESS_KEY=$KMS_ACCESS_KEY KMS_SECRET_KEY=$KMS_SECRET_KEY KMS_KEY_ARN=$KMS_KEY_ARN +GITEA_APP_TOKEN=$GITEA_APP_TOKEN diff --git a/ansible/tasks/nvidia-cdi-generator.yml b/ansible/tasks/nvidia-cdi-generator.yml new file mode 100644 index 0000000..c30d581 --- /dev/null +++ b/ansible/tasks/nvidia-cdi-generator.yml @@ -0,0 +1,28 @@ +- name: Install nvidia-cdi-generator + become: true + ansible.builtin.package: + name: + - nvidia-container-toolkit + state: present + +- name: Create systemd service file for nvidia-cdi-generator + become: true + ansible.builtin.copy: + src: services/nvidia-cdi-generator.service + dest: /etc/systemd/system/nvidia-cdi-generator.service + owner: root + group: root + mode: "0644" + +- name: Reload systemd daemon + become: true + ansible.builtin.systemd: + daemon_reload: true + name: nvidia-cdi-generator + +- name: Enable nvidia-cdi-generator service + become: true + ansible.builtin.systemd: + name: nvidia-cdi-generator + enabled: true + state: started diff --git a/ansible/tasks/podman.yml b/ansible/tasks/podman.yml new file mode 100644 index 0000000..da55f78 --- /dev/null +++ b/ansible/tasks/podman.yml @@ -0,0 +1,241 @@ +- name: Check if required vars are set + ansible.builtin.fail: + msg: "Required variables are not set for Podman setup. Please check your playbook variables.)" + when: + - podman_user is not defined + - podman_group is not defined + - podman_user_home is not defined + +- name: Set fact podman_binary (Debian family) + when: ansible_os_family == 'Debian' + ansible.builtin.set_fact: + podman_binary: /usr/bin/podman + +- name: Set fact podman_binary (Red Hat family) + when: ansible_os_family == 'RedHat' + ansible.builtin.set_fact: + podman_binary: /usr/sbin/podman + +- name: Install Podman and its dependencies + become: true + ansible.builtin.package: + name: + - podman + - acl + state: present + +- name: Create group and user for AI workloads + become: true + ansible.builtin.group: + name: "{{ podman_group }}" + system: true + +- name: Create user and group for AI containers + become: true + ansible.builtin.user: + name: "{{ podman_user }}" + groups: "{{ podman_group }}" + shell: /bin/bash + home: "{{ podman_user_home }}" + group: "{{ podman_group }}" + +- name: Disable password login for AI user + ansible.builtin.command: passwd -d "{{ podman_user }}" + become: true + changed_when: false + +- name: SELinux tasks (RedHat only) + when: ansible_os_family == 'RedHat' + become: true + block: + - name: Persistently set SELinux context for home directories on AI home + community.general.sefcontext: + target: "{{ podman_user_home }}(/.*)?" + setype: user_home_dir_t + state: present + + - name: Apply SELinux context changes to AI home directories + ansible.builtin.command: restorecon -Rv {{ podman_user_home }} + changed_when: false + +- name: Create user home + become: true + ansible.builtin.file: + path: "{{ podman_user_home }}" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_group }}" + mode: "0750" + +- name: Create build folders + become: true + ansible.builtin.file: + path: "{{ podman_user_home }}/build" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_group }}" + mode: "0750" + +- name: Create folders + become: true + ansible.builtin.file: + path: "{{ podman_user_home }}/{{ item.name }}" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_group }}" + mode: "0750" + loop: "{{ pods }}" + +- name: Enable sudo access to podman user (temporary) + become: true + ansible.builtin.lineinfile: + path: "/etc/sudoers.d/ansible_{{ podman_user }}_tmp" + create: true + mode: "0440" + line: "{{ ansible_user_id }} ALL=({{ podman_user }}) NOPASSWD: ALL" + validate: "visudo -cf %s" + +- name: Enable lingering for podman user + become: true + ansible.builtin.command: loginctl enable-linger {{ podman_user }} + register: linger_status + changed_when: >- + "'created' in linger_status.stdout or + (linger_status.rc == 0 and not + ('linger file already exists' in linger_status.stderr or + 'linger file does not exist' in linger_status.stderr))" + failed_when: + - linger_status.rc != 0 and not + ('linger file already exists' in linger_status.stderr) + +- name: Get current user's UID + ansible.builtin.command: id -u {{ podman_user }} + changed_when: false + register: uid + +- name: Get current user's GID + ansible.builtin.command: id -g {{ podman_user }} + changed_when: false + register: gid + +- name: Ensure rootless Podman works with overlay driver on Debian-family systems + become: true + vars: + subuid_range: "100000:65536" + block: + - name: Install required packages for rootless Podman overlay support + ansible.builtin.apt: + name: + - uidmap + - fuse-overlayfs + - libslirp0 + - slirp4netns + state: present + update_cache: true + + - name: Ensure subuid entry exists for the user + ansible.builtin.lineinfile: + path: /etc/subuid + line: "{{ podman_user }}:{{ subuid_range }}" + create: true + state: present + regexp: "^{{ podman_user }}:" + mode: "0644" + + - name: Ensure subgid entry exists for the user + ansible.builtin.lineinfile: + path: /etc/subgid + line: "{{ podman_user }}:{{ subuid_range }}" + create: true + state: present + regexp: "^{{ podman_user }}:" + mode: "0644" + + - name: Ensure overlay module is loaded # noqa no-ignore-errors + community.general.modprobe: + name: overlay + state: present + + - name: Ensure overlay module loads on boot + ansible.builtin.lineinfile: + path: /etc/modules + line: overlay + create: true + state: present + mode: "0644" + +- name: Run tasks as podman user + become: true + become_user: "{{ podman_user }}" + block: + - name: Create a custom Podman network + when: podman_network_name is defined and podman_network_name != 'none' + containers.podman.podman_network: + name: "{{ podman_network_name }}" + state: present + + - name: Build containers images + containers.podman.podman_image: + name: "{{ podman_user }}/{{ item.name }}" + path: "{{ podman_user_home }}/build" + build: + cache: false + force_rm: true + container_file: "{{ item.build.dockerfile }}" + extra_args: "--build-arg UID={{ uid.stdout }} --build-arg GID={{ gid.stdout }} --security-opt=label=disable {{ item.build.args | default('') }}" + tag: "latest" + state: present + loop: "{{ pods }}" + when: item.build is defined + + - name: Pull the requested images + containers.podman.podman_image: + name: "{{ item.repo.image }}" + tag: "{{ item.repo.tag | default('latest') }}" + state: present + loop: "{{ pods }}" + when: item.repo is defined + + - name: Create pods + containers.podman.podman_container: + name: "{{ item.name }}" + image: "{{ item.repo.image | default(podman_user + '/' + item.name) }}" + state: started + device: "{{ item.device | default(omit) }}" + env: "{{ item.env | default(omit) }}" + network: "{{ podman_network_name | default(omit) }}" + ports: "{{ item.ports | default(omit) }}" + volumes: "{{ item.volumes | default(omit) }}" + cmd_args: + - "--userns=keep-id" + - "--security-opt=label=disable" + loop: "{{ pods }}" + +- name: Create systemd service file for pods + become: true + ansible.builtin.template: + src: podman.service.j2 + dest: "/etc/systemd/system/podman-{{ item.name }}.service" + owner: root + group: root + mode: "0644" + loop: "{{ pods }}" + +- name: Reload systemd daemon + become: true + ansible.builtin.systemd: + daemon_reload: true + +- name: Enable containers at boot + become: true + ansible.builtin.systemd: + name: "podman-{{ item.name }}" + enabled: true + state: started + loop: "{{ pods }}" + +- name: Remove temporary sudo access + become: true + ansible.builtin.file: + path: "/etc/sudoers.d/ansible_{{ podman_user }}_tmp" + state: absent diff --git a/ansible/templates/podman.service.j2 b/ansible/templates/podman.service.j2 index 583aa2f..cc307e6 100644 --- a/ansible/templates/podman.service.j2 +++ b/ansible/templates/podman.service.j2 @@ -4,11 +4,11 @@ After=local-fs.target network-online.target nvidia-cdi-generator.service [Service] Type=oneshot -User={{ ai_user}} -Group={{ ai_group }} +User={{ podman_user }} +Group={{ podman_group }} RemainAfterExit=true -ExecStart=/usr/sbin/podman start {{ item.name }} -ExecStop=/usr/sbin/podman stop {{ item.name }} +ExecStart={{ podman_binary }} start {{ item.name }} +ExecStop={{ podman_binary }} stop {{ item.name }} StandardOutput=journal StandardError=journal diff --git a/inventory/hosts b/inventory/hosts index 6363f78..ed99a85 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -68,5 +68,8 @@ dev-01.alexpires.dev vh-01.alexpires.dev gpu-01.alexpires.dev -[ai] +[openwebui] gpu-01.alexpires.dev + +[mcp] +dev-01.alexpires.dev diff --git a/terraform/cloudns-iac/config.tf b/terraform/cloudns-iac/config.tf index 0c43282..c6daf0d 100644 --- a/terraform/cloudns-iac/config.tf +++ b/terraform/cloudns-iac/config.tf @@ -177,7 +177,7 @@ locals { }, { name = "ai" - type = "CNAME" + type = "CNAME" value = "${local.vps_1.name}.alexpires.me" }, ] diff --git a/terraform/k8sapps/apps.tf b/terraform/k8sapps/apps.tf index 031e596..34c4b15 100644 --- a/terraform/k8sapps/apps.tf +++ b/terraform/k8sapps/apps.tf @@ -145,4 +145,5 @@ module "reverse_proxy" { services = local.reverse_proxy_services wireguard_config = local.reverse_proxy_wireguard_config + auth_token = var.nginx_token_bearer } diff --git a/terraform/k8sapps/config.tf b/terraform/k8sapps/config.tf index 6698ba2..2f5fcb0 100644 --- a/terraform/k8sapps/config.tf +++ b/terraform/k8sapps/config.tf @@ -88,6 +88,7 @@ locals { dns = "10.19.4.1" allowed_ips = [ "10.19.4.106/32", + "10.19.4.136/32", ] } @@ -106,11 +107,17 @@ locals { "Connection" = "upgrade" } }, + { + private = true + path = "/tools/gitea" + upstream = "http://10.19.4.136:8000" + rewrite = "^/tools/gitea/(.*)$ /$1 break" + }, { path = "/" }, ] - } + }, } diff --git a/terraform/k8sapps/secrets.tf b/terraform/k8sapps/secrets.tf index f4c2b8d..0588c8c 100644 --- a/terraform/k8sapps/secrets.tf +++ b/terraform/k8sapps/secrets.tf @@ -134,3 +134,9 @@ variable "wireguard_1_pre_shared_key" { type = string sensitive = true } + +variable "nginx_token_bearer" { + description = "Token bearer for nginx" + type = string + sensitive = true +} diff --git a/terraform/k8sapps/sectool.env b/terraform/k8sapps/sectool.env index 669a0ba..76df3bf 100644 --- a/terraform/k8sapps/sectool.env +++ b/terraform/k8sapps/sectool.env @@ -34,3 +34,5 @@ TF_VAR_server_api_access_token=$SERVER_API_ACCESS_TOKEN TF_VAR_wireguard_1_private_key=$WIREGUARD_1_PRIVATE_KEY TF_VAR_wireguard_1_public_key=$WIREGUARD_1_PUBLIC_KEY TF_VAR_wireguard_1_pre_shared_key=$WIREGUARD_1_PRE_SHARED_KEY +TF_VAR_nginx_token_bearer=$NGINX_TOKEN_BEARER + diff --git a/terraform/modules/apps/nginx-reverse-proxy/config.tf b/terraform/modules/apps/nginx-reverse-proxy/config.tf index 69f4577..9d06e37 100644 --- a/terraform/modules/apps/nginx-reverse-proxy/config.tf +++ b/terraform/modules/apps/nginx-reverse-proxy/config.tf @@ -29,13 +29,20 @@ locals { upstream = v.upstream tls = v.tls fqdn = join(" ", v.fqdn) + auth = length([for l in v.locations : true if l.private]) > 0 locations = [for l in v.locations : { - path = l.path - headers = merge(l.headers, coalesce(v.default_headers, local.default_headers)) + path = l.path + headers = merge(l.headers, coalesce(v.default_headers, local.default_headers)) + private = l.private + upstream = l.upstream + rewrite = l.rewrite }] custom_snippet = v.custom_snippet })) } services_config_checksum = sha256(jsonencode(local.services_config)) + + auth_script = file("${path.module}/resources/auth/auth_server.py") + auth_script_checksum = sha256(local.auth_script) } diff --git a/terraform/modules/apps/nginx-reverse-proxy/main.tf b/terraform/modules/apps/nginx-reverse-proxy/main.tf index 288a634..2073782 100644 --- a/terraform/modules/apps/nginx-reverse-proxy/main.tf +++ b/terraform/modules/apps/nginx-reverse-proxy/main.tf @@ -102,10 +102,21 @@ resource "kubernetes_config_map" "nginx_default_config" { data = local.services_config } +resource "kubernetes_config_map" "auth_script" { + metadata { + name = "auth-script" + namespace = kubernetes_namespace.reverse_proxy.metadata[0].name + } + + data = { + "auth_server.py" = local.auth_script + } +} + resource "kubernetes_ingress_v1" "reverse-proxy_ingress" { for_each = var.services metadata { - name = "reverse-proxy-ingress" + name = format("reverse-proxy-%s", replace(each.key, ".", "-")) namespace = kubernetes_namespace.reverse_proxy.metadata[0].name annotations = merge({ @@ -145,6 +156,19 @@ resource "kubernetes_ingress_v1" "reverse-proxy_ingress" { } } +resource "kubernetes_secret" "auth_token" { + metadata { + name = "auth-token-secret" + namespace = kubernetes_namespace.reverse_proxy.metadata[0].name + } + + data = { + token = var.auth_token + } + + type = "Opaque" +} + resource "kubernetes_deployment" "reverse_proxy" { metadata { @@ -175,9 +199,10 @@ resource "kubernetes_deployment" "reverse_proxy" { app = "reverse-proxy" } annotations = { - "checksum/nginx" = local.nginx_config_checksum - "checksum/services" = local.services_config_checksum - "checksum/wireguard" = local.wireguard_config_checksum + "checksum/nginx" = local.nginx_config_checksum + "checksum/services" = local.services_config_checksum + "checksum/wireguard" = local.wireguard_config_checksum + "checksum/auth_server" = local.auth_script_checksum } } spec { @@ -242,6 +267,34 @@ resource "kubernetes_deployment" "reverse_proxy" { } } + container { + name = "auth-sidecar" + image = "python:3.11-slim" + + command = ["python", "/app/auth_server.py"] + + env { + name = "AUTH_TOKEN" + value_from { + secret_key_ref { + name = "auth-token-secret" + key = "token" + } + } + } + + volume_mount { + name = "auth-script" + mount_path = "/app" + read_only = true + } + + port { + container_port = 5000 + name = "auth" + } + } + container { name = "reverse-proxy" image = "nginx:${var.tag}" @@ -334,6 +387,13 @@ resource "kubernetes_deployment" "reverse_proxy" { } } + volume { + name = "auth-script" + config_map { + name = kubernetes_config_map.auth_script.metadata[0].name + } + } + volume { name = "nginx" config_map { diff --git a/terraform/modules/apps/nginx-reverse-proxy/resources/auth/auth_server.py b/terraform/modules/apps/nginx-reverse-proxy/resources/auth/auth_server.py new file mode 100644 index 0000000..1385f06 --- /dev/null +++ b/terraform/modules/apps/nginx-reverse-proxy/resources/auth/auth_server.py @@ -0,0 +1,40 @@ +import os +import logging +from http.server import BaseHTTPRequestHandler, HTTPServer + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" +) + +EXPECTED_TOKEN = os.getenv("AUTH_TOKEN", "") + +class AuthHandler(BaseHTTPRequestHandler): + def do_GET(self): + auth = self.headers.get('Authorization') + + # Log request info + client_ip = self.client_address[0] + logging.info(f"Auth request from {client_ip}") + + if auth == f"Bearer {EXPECTED_TOKEN}": + logging.info("Authentication successful") + self.send_response(200) + else: + logging.warning("Authentication failed") + self.send_response(401) + self.end_headers() + + def log_message(self, format, *args): + # Suppress default BaseHTTPRequestHandler logging + return + +if __name__ == '__main__': + if not EXPECTED_TOKEN: + logging.error("AUTH_TOKEN environment variable not set. Exiting.") + exit(1) + + server = HTTPServer(('', 5000), AuthHandler) + logging.info("Auth server running on port 5000...") + server.serve_forever() diff --git a/terraform/modules/apps/nginx-reverse-proxy/resources/nginx/conf.d/default.conf.tpl b/terraform/modules/apps/nginx-reverse-proxy/resources/nginx/conf.d/default.conf.tpl index fdff5a7..a8acd21 100644 --- a/terraform/modules/apps/nginx-reverse-proxy/resources/nginx/conf.d/default.conf.tpl +++ b/terraform/modules/apps/nginx-reverse-proxy/resources/nginx/conf.d/default.conf.tpl @@ -10,9 +10,24 @@ server { %{ if custom_snippet != "" ~} ${custom_snippet} %{ endif ~} +%{ if auth ~} +location = /auth { + internal; + proxy_pass http://localhost:5000/; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Authorization $http_authorization; +} +%{ endif } %{ for l in locations ~} location ${l.path} { - proxy_pass ${upstream}; +%{ if l.private ~} + auth_request /auth; +%{ endif ~} +%{ if l.rewrite != "" ~} + rewrite ${l.rewrite}; +%{ endif ~} + proxy_pass %{ if l.upstream != "" ~}${l.upstream}%{ else ~}${upstream}%{ endif ~}; proxy_http_version 1.1; %{ for k,v in l.headers ~} proxy_set_header ${k} ${v}; diff --git a/terraform/modules/apps/nginx-reverse-proxy/variables.tf b/terraform/modules/apps/nginx-reverse-proxy/variables.tf index 69b366e..5790645 100644 --- a/terraform/modules/apps/nginx-reverse-proxy/variables.tf +++ b/terraform/modules/apps/nginx-reverse-proxy/variables.tf @@ -36,10 +36,19 @@ variable "services" { https_port = optional(number, 443) tls = optional(bool, false) locations = optional(list(object({ - path = string, - headers = optional(map(string), {}), + path = string, + headers = optional(map(string), {}), + private = optional(bool, false), + upstream = optional(string, ""), + rewrite = optional(string, "") })), []) custom_snippet = optional(string, "") config = optional(string, "") })) } + +variable "auth_token" { + description = "The authentication token to use for the reverse proxy" + type = string + sensitive = true +}