diff --git a/ReadMe.md b/ReadMe.md index d5a394e..84f38b2 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -111,12 +111,12 @@ Static secrets, such as API tokens and passwords, are stored as **sensitive vari > **Note**: Some non-secret values (like `public_facing_ip`) are marked as sensitive to prevent them from being exposed in public logs or plan outputs. -### Dynamic Secrets +'''### Dynamic Secrets For services deployed by the `proxy_service_stack` module, credentials are not stored statically. Instead: -1. **Generated Secrets**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will generate a unique, high-entropy value for each and inject it into the container's environment variables. +1. **Infisical Integration**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will fetch these secrets from Infisical Cloud and inject them into the container's environment variables. 2. **OAuth Credentials**: For services using OAuth, the `docker-stack` module automatically creates an OAuth2 provider in Authentik. The resulting `client_id` and `client_secret` are then injected as environment variables into the container. -This ensures that secrets are managed dynamically and securely, with minimal manual intervention. +This ensures that secrets are managed dynamically and securely, with minimal manual intervention.''' ## ⚠️ Operational Notes diff --git a/config/services/authentik.yaml b/config/services/authentik.yaml deleted file mode 100644 index ef00236..0000000 --- a/config/services/authentik.yaml +++ /dev/null @@ -1,2 +0,0 @@ -authentik: - admin-user: "" \ No newline at end of file diff --git a/config/services/deluge-vpn.yaml b/config/services/deluge-vpn.yaml index a2a9be7..7adfe82 100644 --- a/config/services/deluge-vpn.yaml +++ b/config/services/deluge-vpn.yaml @@ -1,6 +1,6 @@ deluge-vpn: dns: - enabled: true + enabled: false network: internal: true service_port: 8112 @@ -8,18 +8,19 @@ deluge-vpn: - name: "br1" ip_address: "192.168.5.28" auth: - enabled: true - proxy: true + enabled: false group: "Arr" + secrets: + VPN_USER: "VPN_USER" + VPN_PASS: "VPN_PASS" icon: "https://vectorified.com/images/deluge-icon-3.jpg" service_name: "deluge-vpn" image_name: "binhex/arch-delugevpn:2.2" - domain_name: "prowlarr.dcapi.app" mounts: - "/etc/localtime:/etc/localtime:ro" - "/mnt/user/Arr/deluge-data:/config" - "/mnt/user/Downloads:/data/downloads" - - "/mnt/user/appdata/binhex-delugevpn/openvpn:/config/openvpn" + - "/mnt/user/Arr/deluge-data/openvpn:/config/openvpn" capabilities: add: - "CAP_NET_ADMIN" @@ -34,4 +35,5 @@ deluge-vpn: - "VPN_ENABLED=yes" - "VPN_PROV=custom" - "VPN_CLIENT=openvpn" - - "LAN_NETWORK=192.168.1.0/24" \ No newline at end of file + - "LAN_NETWORK=192.168.1.0/24" + - "UPDATE_VAL=1" \ No newline at end of file diff --git a/config/services/flaresolverr.yaml b/config/services/flaresolverr.yaml index c60189a..73b0ed1 100644 --- a/config/services/flaresolverr.yaml +++ b/config/services/flaresolverr.yaml @@ -1,11 +1,13 @@ flaresolverr: - service_name: "flaresolverr" - image_name: "flaresolverr/flaresolverr:v3.3.25" + dns: + enabled: false network: internal: true service_port: 8112 networks: - name: "br1" - ip_address: "192.168.5.27" - env: - - "TZ=America/Chicago" + ip_address: "192.168.5.29" + auth: + enabled: false + service_name: "flaresolverr" + image_name: "flaresolverr/flaresolverr:v3.3.25" \ No newline at end of file diff --git a/config/services/requestrr.yaml b/config/services/requestrr.yaml index 0543fbf..1310436 100644 --- a/config/services/requestrr.yaml +++ b/config/services/requestrr.yaml @@ -1,12 +1,16 @@ requestrr: - service_name: "requestrr" - image_name: "linuxserver/requestrr:2.1.2" + dns: + enabled: false network: internal: true service_port: 4545 networks: - name: "br1" ip_address: "192.168.5.27" + auth: + enabled: false + service_name: "requestrr" + image_name: "linuxserver/requestrr:2.1.2" mounts: - "/mnt/user/Arr/requestrr-data:/config" - "/etc/localtime:/etc/localtime:ro" \ No newline at end of file diff --git a/config/stacks/arr.yaml b/config/stacks/arr.yaml index 51df0f9..5a50c25 100644 --- a/config/stacks/arr.yaml +++ b/config/stacks/arr.yaml @@ -21,7 +21,10 @@ ip_address: "192.168.5.22" auth: enabled: true - proxy: true + proxy: + enabled: true + user_secret: "prowlarr_username" + pass_secret: "prowlarr_password" group: "Arr" icon: "https://static-00.iconduck.com/assets.00/prowlarr-icon-512x512-v9ekdjxx.png" service_name: "prowlarr" @@ -41,7 +44,10 @@ ip_address: "192.168.5.23" auth: enabled: true - proxy: true + proxy: + enabled: true + user_secret: "sonarr_username" + pass_secret: "sonarr_password" group: "Arr" icon: "https://static-00.iconduck.com/assets.00/sonarr-icon-1024x1024-wkay604k.png" service_name: "sonarr" @@ -49,6 +55,8 @@ domain_name: "sonarr.dcapi.app" mounts: - "/mnt/user/Arr/sonarr-data:/config" + - "/mnt/user/Media:/media" + - "/mnt/user/Downloads:/downloads" radarr: dns: enabled: true @@ -61,7 +69,10 @@ ip_address: "192.168.5.24" auth: enabled: true - proxy: true + proxy: + enabled: true + user_secret: "radarr_username" + pass_secret: "radarr_password" group: "Arr" icon: "https://static-00.iconduck.com/assets.00/radarr-icon-462x512-bydv4e4f.png" service_name: "radarr" @@ -69,6 +80,8 @@ domain_name: "radarr.dcapi.app" mounts: - "/mnt/user/Arr/radarr-data:/config" + - "/mnt/user/Media:/media" + - "/mnt/user/Downloads:/downloads" lidarr: dns: enabled: true @@ -81,9 +94,14 @@ ip_address: "192.168.5.25" auth: enabled: true - proxy: true + proxy: + enabled: true + user_secret: "lidarr_username" + pass_secret: "lidarr_password" group: "Arr" service_name: "lidarr" image_name: "linuxserver/lidarr:2.13.0-develop" mounts: - - "/mnt/user/Arr/lidarr-data:/config" \ No newline at end of file + - "/mnt/user/Arr/lidarr-data:/config" + - "/mnt/user/Media:/media" + - "/mnt/user/Downloads:/downloads" \ No newline at end of file diff --git a/config/system.yaml b/config/system.yaml index f1a39be..b6c595a 100644 --- a/config/system.yaml +++ b/config/system.yaml @@ -8,4 +8,8 @@ zones: admin_username: "DCCoder" existing_networks: - "br1" - - "br0" \ No newline at end of file + - "br0" +infisical: + project: "home-net-ln-sy" + environment: "dev" + folder: "/secrets" \ No newline at end of file diff --git a/docs/service-config.md b/docs/service-config.md index c0b05db..8169d75 100644 --- a/docs/service-config.md +++ b/docs/service-config.md @@ -13,7 +13,23 @@ The structure of each top-level key in these files is **not standardized**. It i Below are examples of different structures found in this project. -### Example 1: A Standalone Docker Service (`deluge-vpn`) +'''### Example 1: A Standalone Docker Service (`flaresolverr`) + +This service is consumed by the `flaresolverr_service` module in `services.tf`, which is a wrapper around the generic `docker` module. + +```yaml +flaresolverr: + service_name: "flaresolverr" + image_name: "flaresolverr/flaresolverr:latest" + network: + networks: + - "br1" + env: + - "LOG_LEVEL=info" + - "TZ=America/New_York" +``` + +### Example 2: A Standalone Docker Service (`deluge-vpn`) This service is consumed by the `delugevpn_service` module in `services.tf`, which is a wrapper around the generic `docker` module. @@ -31,7 +47,7 @@ deluge-vpn: capabilities: add: - "NET_ADMIN" -``` +```'' ### Example 2: A Global Configuration Object (`authentik`) diff --git a/docs/stack-config.md b/docs/stack-config.md index c5d56c8..57a1b29 100644 --- a/docs/stack-config.md +++ b/docs/stack-config.md @@ -143,11 +143,3 @@ your_stack_name: * **`auth.oauth.redirect_uris`**: A list of additional relative paths (e.g., `/oauth/callback`) that will be appended to the service's domain name to form the complete, valid OAuth redirect URIs required by Authentik. -## Generated Secrets Workflow - -* **`generated_secrets` (Stack Level)**: A list of string names (e.g., `"API_KEY"`) for secrets that your services will consume. - * **Workflow**: - 1. **Definition**: You must first define these secret names in the `config/secrets.yaml` file. - 2. **Generation & Storage**: The `generated_secrets` Terraform module (run as part of the root module) reads `config/secrets.yaml`, generates a unique, value for each listed secret, and then securely stores these generated values in Infisical Cloud. - 3. **Consumption by Stacks**: When you list a secret name here, the `docker-stack` module will automatically pull the corresponding secret value from Infisical and inject it as an environment variable into the container. - * **Important Note**: Any secret name listed in a stack's `generated_secrets` field *must* first be defined in `config/secrets.yaml` to ensure it is generated and stored in Infisical. If a requested secret is not found, the Terraform plan will fail, preventing deployment with missing credentials. \ No newline at end of file diff --git a/terraform/modules/docker-service/auth.tf b/terraform/modules/docker-service/auth.tf new file mode 100644 index 0000000..28dfac1 --- /dev/null +++ b/terraform/modules/docker-service/auth.tf @@ -0,0 +1,50 @@ + + +module "proxy_authentication" { + source = "../proxy_auth" + count = var.service.auth.enabled && var.service.auth.proxy.enabled ? 1 : 0 + + group = var.service.auth.group + description = var.service.description + # If a static IP is defined, use it. Otherwise, fall back to the service name, + # which is resolvable within a Docker network. + internal_host = "http://${coalesce(local.service_ip_address, var.service.service_name)}:${var.service.network.service_port}" + external_host = var.service.dns.domain_name + name = var.service.service_name + username_attribute = "${var.service.service_name}_username" + password_attribute = "${var.service.service_name}_password" + create_access_group = true + access_group_name = "tf_${var.service.service_name}" + user_to_add_to_access_group = var.system.network_admin_username + access_group_attributes = jsonencode( + { + "${var.service.service_name}_username" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.user_secret, null), "${var.service.service_name}_username")].value, + "${var.service.service_name}_password" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.pass_secret, null), "${var.service.service_name}_password")].value + } + ) +} + +module "oauth_authentication" { + source = "../oauth_auth" + count = var.service.auth.enabled && var.service.auth.oauth.enabled ? 1 : 0 + + group = var.service.auth.group + description = var.service.description + name = var.service.service_name + create_access_group = true + access_group_name = "tf_${var.service.service_name}" + user_to_add_to_access_group = var.system.network_admin_username + allowed_redirect_uris = concat( + [ + { + matching_mode = "strict", + url = "https://${var.service.dns.domain_name}" } + ], + [ + for uri_path in coalesce(var.service.auth.oauth.redirect_uris, []) : { + matching_mode = "strict", + url = "https://${var.service.dns.domain_name}/${uri_path}" + } + ] + ) +} diff --git a/terraform/modules/docker-service/dns.tf b/terraform/modules/docker-service/dns.tf new file mode 100644 index 0000000..0efdf2f --- /dev/null +++ b/terraform/modules/docker-service/dns.tf @@ -0,0 +1,21 @@ +data "nginxproxymanager_access_lists" "access_lists" {} + +module "service_dns" { + source = "../dns" + count = var.service.dns.enabled ? 1 : 0 + + internal_only = var.service.network.internal + service_port = var.service.auth.proxy.enabled ? var.system.authentik.port : var.service.network.service_port + zone_name = var.zone_name + domain_name = var.service.dns.domain_name + + # Really don't like having the ACLs hardcoded here... + access_list_id = var.service.network.internal ? local.npm_access_lists_by_name["Internal Only"] : local.npm_access_lists_by_name["Cloudflare"] + internal_host_ipv4 = var.system.proxy_ip + # If not using proxy auth, point to the service's static IP. If no static IP, + # fall back to the service name, which NPM can use as a hostname. + service_ipv4 = var.service.auth.proxy.enabled ? var.system.authentik.ip_address : coalesce(local.service_ip_address, var.service.service_name) + admin_email = var.system.network_admin_email + dns_cloudflare_api_token = var.system.cloudflare_api_token + external_host_ipv4 = var.system.public_facing_ip +} \ No newline at end of file diff --git a/terraform/modules/docker-service/locals.tf b/terraform/modules/docker-service/locals.tf new file mode 100644 index 0000000..61d64a1 --- /dev/null +++ b/terraform/modules/docker-service/locals.tf @@ -0,0 +1,25 @@ +locals { + # Determine the primary IP address for the service from its network configuration. + service_ip_address = try( + try([for n in var.service.network.networks : n.ip_address if n.name == "br1" && n.ip_address != null][0], null), + try([for n in var.service.network.networks : n.ip_address if n.name == "br0" && n.ip_address != null][0], null), + try([for n in var.service.network.networks : n.ip_address if n.ip_address != null][0], null) + ) + + # Generate environment variables for OAuth if it's enabled for the service. + oauth_envs = var.service.auth.oauth.enabled ? [ + for env_name, output_key in var.service.auth.oauth.keys : + format( + "%s=%s", + env_name, + { + "client_id" = module.oauth_authentication[0].client_id, + "client_secret" = module.oauth_authentication[0].client_secret, + "well_known_url" = module.oauth_authentication[0].provider_info_url + }[output_key] + ) + ] : [] + + # Create a map of Nginx Proxy Manager access lists by name for easy lookup. + npm_access_lists_by_name = { for al in data.nginxproxymanager_access_lists.access_lists.access_lists : al.name => al.id } +} diff --git a/terraform/modules/docker-service/main.tf b/terraform/modules/docker-service/main.tf new file mode 100644 index 0000000..d94ac01 --- /dev/null +++ b/terraform/modules/docker-service/main.tf @@ -0,0 +1,21 @@ + + +module "service_container" { + source = "../../modules/docker" + + icon = var.service.icon + web_ui = try(var.service.network.service_port, null) != null && local.service_ip_address != null ? "http://${local.service_ip_address}:${var.service.network.service_port}" : null + container_name = var.service.service_name + container_image = var.service.image_name + container_network_mode = var.service.network_mode + enable_gpu = var.service.enable_gpu + environment_vars = toset(concat(local.secret_envs, coalesce(local.oauth_envs, []), coalesce(var.service.env, []))) + mounts = var.service.mounts + container_capabilities = var.service.capabilities + commands = var.service.commands + + # Attach the container to custom networks defined in the stack, but only if the service + # explicitly lists that network in its own configuration. + # The docker module expects a list of objects with `name` and `ipv4_address`. + networks = coalesce(var.service.network.networks, []) +} diff --git a/terraform/modules/docker-service/secrets.tf b/terraform/modules/docker-service/secrets.tf new file mode 100644 index 0000000..50abd63 --- /dev/null +++ b/terraform/modules/docker-service/secrets.tf @@ -0,0 +1,23 @@ +data "infisical_projects" "home-net" { + # This assumes a constant project slug. Consider making this configurable + # via var.system if it needs to be more dynamic. + slug = var.system.infisical.project +} + +data "infisical_secrets" "secrets" { + # Only fetch secrets if the stack configuration requests them. + count = (var.service.secrets != null && length(var.service.secrets) > 0) || (try(var.service.auth.proxy.enabled, false)) ? 1 : 0 + + env_slug = var.system.infisical.environment + workspace_id = data.infisical_projects.home-net.id + # This path corresponds to where the root `secrets` module stores secrets. + folder_path = var.system.infisical.folder +} + +locals{ + # Create a list of environment variables from the secrets map. + secret_envs = (var.service.secrets != null && length(var.service.secrets) > 0) ? [ + for key, value in var.service.secrets : + format("%s=%s", key, data.infisical_secrets.secrets[0].secrets[value].value) + ] : [] +} \ No newline at end of file diff --git a/terraform/modules/docker-service/variables.tf b/terraform/modules/docker-service/variables.tf new file mode 100644 index 0000000..c82a65d --- /dev/null +++ b/terraform/modules/docker-service/variables.tf @@ -0,0 +1,71 @@ +variable "service" { + type = object({ + # --- Core Service Definition --- + service_name = string + image_name = string + description = optional(string, "") + icon = optional(string, "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/docker.png") + + # --- Execution & Runtime --- + commands = optional(list(string)) + network_mode = optional(string) + enable_gpu = optional(bool, false) + capabilities = optional(object({ + add = optional(list(string)) + drop = optional(list(string)) + }), {}) + + # --- Storage --- + mounts = optional(list(string)) + volumes = optional(list(string)) + + # --- Environment & Secrets --- + env = optional(list(string)) + secrets = optional(map(string)) + + # --- Networking & DNS --- + network = optional(object({ + internal = optional(bool, false) + service_port = optional(number) + networks = optional(list(object({ + name = string + ip_address = optional(string) + })), []) + })) + dns = object({ + enabled = optional(bool, false), + internal = optional(bool, true), + domain_name = optional(string) + }) + + # --- Authentication --- + auth = optional(object({ + enabled = optional(bool, false) + group = optional(string, "Uncategorized") + proxy = optional(object({ + enabled = optional(bool, false) + user_secret = optional(string) + pass_secret = optional(string) + }), {}) + oauth = optional(object({ + enabled = optional(bool, false), + keys = optional(map(string), {}), + scopes = optional(list(string)), + redirect_uris = optional(list(string)) + }), {}) + }), {}) + }) + + description = "Service and it's configuration" +} + +variable "zone_name"{ + type = string + default = "" + description = "DNS Zone name for service if DNS enabled." +} + +variable "system" { + type = any + description = "A dynamic object containing system-wide configuration variables. Its attributes are accessed via lookup() for flexibility." +} \ No newline at end of file diff --git a/terraform/modules/docker-service/versions.tf b/terraform/modules/docker-service/versions.tf new file mode 100644 index 0000000..b08db4f --- /dev/null +++ b/terraform/modules/docker-service/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + } + nginxproxymanager = { + source = "Sander0542/nginxproxymanager" + version = "1.2.0" + } + infisical = { + source = "infisical/infisical" + version = "0.15.19" + } + } +} \ No newline at end of file diff --git a/terraform/modules/docker-stack/auth.tf b/terraform/modules/docker-stack/auth.tf deleted file mode 100644 index fc2ca36..0000000 --- a/terraform/modules/docker-stack/auth.tf +++ /dev/null @@ -1,64 +0,0 @@ - -resource "random_password" "service_password" { - for_each = { - for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(v, "auth", {}), "proxy", false) == true - } - - length = 24 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - - -module "proxy_authentication" { - source = "../proxy_auth" - for_each = { - for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(v, "auth", {}), "proxy", false) == true - } - - group = each.value.auth.group - description = each.value.description - # If a static IP is defined, use it. Otherwise, fall back to the service name, - # which is resolvable within a Docker network. - internal_host = "http://${coalesce(local.service_ip_addresses[each.key], each.value.service_name)}:${each.value.network.service_port}" - external_host = each.value.dns.domain_name - name = each.value.service_name - username_attribute = "${each.value.service_name}_username" - password_attribute = "${each.value.service_name}_password" - create_access_group = true - access_group_name = "tf_${each.value.service_name}" - user_to_add_to_access_group = var.system.network_admin_username - access_group_attributes = jsonencode( - { - "${each.value.service_name}_username" : "admin", - "${each.value.service_name}_password" : random_password.service_password[each.key].result - } - ) -} - -module "oauth_authentication" { - source = "../oauth_auth" - for_each = { - for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(lookup(v, "auth", {}), "oauth", {}), "enabled", false) == true - } - - group = each.value.auth.group - description = each.value.description - name = each.value.service_name - create_access_group = true - access_group_name = "tf_${each.value.service_name}" //Update to allow for using custom groups rather than just generated - user_to_add_to_access_group = var.system.network_admin_username //Update to allow for a list of users - allowed_redirect_uris = concat( - [ - { - matching_mode = "strict", - url = "https://${each.value.dns.domain_name}" } - ], - [ - for uri_path in coalesce(each.value.auth.oauth.redirect_uris, []) : { - matching_mode = "strict", - url = "https://${each.value.dns.domain_name}/${uri_path}" - } - ] - ) -} diff --git a/terraform/modules/docker-stack/dns.tf b/terraform/modules/docker-stack/dns.tf deleted file mode 100644 index f8918a3..0000000 --- a/terraform/modules/docker-stack/dns.tf +++ /dev/null @@ -1,23 +0,0 @@ -data "nginxproxymanager_access_lists" "access_lists" {} - -module "service_dns" { - source = "../dns" - for_each = { - for k, v in var.stack.services : k => v if lookup(v, "dns").enabled == true - } - - internal_only = each.value.network.internal - service_port = each.value.auth.proxy ? var.system.authentik.port : each.value.network.service_port - zone_name = var.stack.zone_name - domain_name = each.value.dns.domain_name - - # Really don't like having the ACLs hardcoded here... - access_list_id = each.value.network.internal ? local.npm_access_lists_by_name["Internal Only"] : local.npm_access_lists_by_name["Cloudflare"] - internal_host_ipv4 = var.system.proxy_ip - # If not using proxy auth, point to the service's static IP. If no static IP, - # fall back to the service name, which NPM can use as a hostname. - service_ipv4 = each.value.auth.proxy ? var.system.authentik.ip_address : coalesce(local.service_ip_addresses[each.key], each.value.service_name) - admin_email = var.system.network_admin_email - dns_cloudflare_api_token = var.system.cloudflare_api_token - external_host_ipv4 = var.system.public_facing_ip -} \ No newline at end of file diff --git a/terraform/modules/docker-stack/locals.tf b/terraform/modules/docker-stack/locals.tf index 836770a..f0bd6f6 100644 --- a/terraform/modules/docker-stack/locals.tf +++ b/terraform/modules/docker-stack/locals.tf @@ -1,66 +1,25 @@ locals { + # Create a map of service IP addresses, trying different network names. service_ip_addresses = { - for service_key, service_config in var.stack.services : service_key => try( - [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br1" && n.ip_address != null][0], - [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br0" && n.ip_address != null][0], - [for n in try(service_config.network.networks, []) : n.ip_address if n.ip_address != null][0], - null - ) + # This logic finds the first available IP address for a service based on a preferred network order (br1, then br0, then any). + # It safely handles cases where a service has no network configuration, no networks defined, or no static IP on any network. + # It concatenates lists of IPs from each check and adds a final `[null]` to ensure the list is never empty, then takes the first element. + for service_key, service_config in var.stack.services : service_key => concat( + [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br1" && n.ip_address != null], + [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br0" && n.ip_address != null], + [for n in try(service_config.network.networks, []) : n.ip_address if n.ip_address != null], + [null] + )[0] } - # Networks that this module should actually create. - # Filter out pre-existing host networks. + # Filter out networks that already exist. creatable_networks = { for name, config in coalesce(var.stack.networks, {}) : name => config if !contains(var.system.existing_networks, name) } - # A map of secrets fetched from Infisical. - # This will fail the plan if a secret listed in `var.stack.generated_secrets` is not found - # in Infisical, which is the desired behavior to prevent deploying with missing credentials. - generated_secrets = { - for secret_name in toset(coalesce(var.stack.generated_secrets, [])) : - secret_name => data.infisical_secrets.generated_secrets[0].secrets[secret_name].value - } - - # For each service, process its environment variables to substitute secret placeholders. - processed_envs = { - for service_key, service_config in var.stack.services : service_key => [ - for env_string in coalesce(service_config.env, []) : ( - # This pattern ensures that for each env var, we get either the string with the secret - # replaced, or the original string if no placeholder was found. - # The one() function enforces a "one secret per line" rule, failing if multiple placeholders exist. - one(concat( - [ - for secret_name, secret_value in local.generated_secrets : - replace(env_string, format("$${%s}", secret_name), secret_value) - if strcontains(env_string, format("$${%s}", secret_name)) - ], - # This list provides the fallback original string if no secret placeholder was found. - !anytrue([for name in keys(local.generated_secrets) : strcontains(env_string, format("$${%s}", name))]) ? [env_string] : [] - )) - ) - ] - } - - oauth_envs = { - for service_key, service_config in var.stack.services : service_key => ( - lookup(lookup(service_config, "auth", {}), "oauth", { enabled = false }).enabled ? [ - for env_name, output_key in lookup(lookup(service_config, "auth", {}), "oauth", { keys = {} }).keys : - format( - "%s=%s", - env_name, - # This map translates the key from your YAML (e.g., "client_id") - # to the corresponding attribute of the created Authentik resource. - { - "client_id" : module.oauth_authentication[service_key].client_id, - "client_secret" : module.oauth_authentication[service_key].client_secret, - "well_known_url" : module.oauth_authentication[service_key].provider_info_url - }[output_key] # Use the value from the YAML keys map to look up the correct output - ) - ] : [] - ) - } - + # Create a map of Nginx Proxy Manager access lists by name. npm_access_lists_by_name = { for al in data.nginxproxymanager_access_lists.access_lists.access_lists : al.name => al.id } -} \ No newline at end of file +} + +data "nginxproxymanager_access_lists" "access_lists" {} diff --git a/terraform/modules/docker-stack/main.tf b/terraform/modules/docker-stack/main.tf index e36bf55..98ccb33 100644 --- a/terraform/modules/docker-stack/main.tf +++ b/terraform/modules/docker-stack/main.tf @@ -4,16 +4,6 @@ data "infisical_projects" "home-net" { slug = "home-net-ln-sy" } -data "infisical_secrets" "generated_secrets" { - # Only fetch secrets if the stack configuration requests them. - count = length(coalesce(var.stack.generated_secrets, [])) > 0 ? 1 : 0 - - env_slug = "dev" - workspace_id = data.infisical_projects.home-net.id - # This path corresponds to where the root `secrets` module stores secrets. - folder_path = "/generated/credentials" -} - module "custom_network" { count = length(local.creatable_networks) > 0 ? 1 : 0 source = "../../modules/docker-network" @@ -22,7 +12,15 @@ module "custom_network" { module "service_container" { for_each = var.stack.services - source = "../../modules/docker" + source = "../../modules/docker-service" + + zone_name = var.stack.zone_name + service = each.value + system = var.system + +/* +Still need to combine environment, mounts, and networks! + icon = each.value.icon web_ui = try(each.value.network.service_port, null) != null && local.service_ip_addresses[each.key] != null ? "http://${local.service_ip_addresses[each.key]}:${each.value.network.service_port}" : null @@ -39,4 +37,5 @@ module "service_container" { # explicitly lists that network in its own configuration. # The docker module expects a list of objects with `name` and `ipv4_address`. networks = coalesce(each.value.network.networks, []) + */ } diff --git a/terraform/modules/docker-stack/outputs.tf b/terraform/modules/docker-stack/outputs.tf index 659b1d9..e69de29 100644 --- a/terraform/modules/docker-stack/outputs.tf +++ b/terraform/modules/docker-stack/outputs.tf @@ -1,5 +0,0 @@ -output "generated_secrets" { - description = "A map of the dynamically generated secrets for the stack." - value = local.generated_secrets - sensitive = true -} \ No newline at end of file diff --git a/terraform/modules/docker-stack/variables.tf b/terraform/modules/docker-stack/variables.tf index 1bbd670..91ea38c 100644 --- a/terraform/modules/docker-stack/variables.tf +++ b/terraform/modules/docker-stack/variables.tf @@ -3,7 +3,6 @@ variable "stack" { env = optional(list(string)) mounts = optional(list(string)) volumes = optional(list(string)) - generated_secrets = optional(list(string)) zone_name = optional(string, null) networks = optional(map(object({ internal = optional(bool, false) @@ -20,7 +19,8 @@ variable "stack" { mounts = optional(list(string)) enable_gpu = optional(bool, false) env = optional(list(string)) - volumes = optional(list(string)) + volumes = optional(list(string)), + secrets = optional(map(string)), capabilities = optional(object({ add = optional(list(string)) drop = optional(list(string)) @@ -30,17 +30,21 @@ variable "stack" { internal = optional(bool, true), domain_name = optional(string) }) - auth = optional(object({ - enabled = optional(bool, false), - proxy = optional(bool, false), - group = optional(string, "Uncategorized"), - oauth = optional(object({ - enabled = optional(bool, false), - keys = optional(map(string), {}), - scopes = optional(list(string)), - redirect_uris = optional(list(string)) - }), {}) - })) + auth = optional(object({ + enabled = optional(bool, false) + group = optional(string, "Uncategorized") + proxy = optional(object({ + enabled = optional(bool, false) + user_secret = optional(string) + pass_secret = optional(string) + }), {}) + oauth = optional(object({ + enabled = optional(bool, false), + keys = optional(map(string), {}), + scopes = optional(list(string)), + redirect_uris = optional(list(string)) + }), {}) + }), {}) network = optional(object({ internal = optional(bool, false) service_port = optional(number) diff --git a/terraform/modules/generated_secrets/README.md b/terraform/modules/generated_secrets/README.md deleted file mode 100644 index 7916d8f..0000000 --- a/terraform/modules/generated_secrets/README.md +++ /dev/null @@ -1,36 +0,0 @@ - -## Requirements - -| Name | Version | -|------|---------| -| [infisical](#requirement\_infisical) | 0.15.19 | - -## Providers - -| Name | Version | -|------|---------| -| [infisical](#provider\_infisical) | 0.15.19 | -| [random](#provider\_random) | n/a | - -## Resources - -| Name | Type | -|------|------| -| [infisical_secret.stored_secret](https://registry.terraform.io/providers/infisical/infisical/0.15.19/docs/resources/secret) | resource | -| [random_password.generated_secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | -| [infisical_projects.home-net](https://registry.terraform.io/providers/infisical/infisical/0.15.19/docs/data-sources/projects) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [name](#input\_name) | List of secret names to generate | `list(string)` | n/a | yes | -| [project\_slug](#input\_project\_slug) | Slug of project to store secret in | `string` | `"home-net-ln-sy"` | no | - -## Outputs - -| Name | Description | -|------|-------------| -| [generated\_secrets\_list](#output\_generated\_secrets\_list) | A list of objects, each containing the name and generated value of a secret. | -| [generated\_secrets\_map](#output\_generated\_secrets\_map) | A map of generated secret names to their values. | - \ No newline at end of file diff --git a/terraform/modules/generated_secrets/locals.tf b/terraform/modules/generated_secrets/locals.tf deleted file mode 100644 index fd06c19..0000000 --- a/terraform/modules/generated_secrets/locals.tf +++ /dev/null @@ -1,17 +0,0 @@ -locals { - # Create a map of secret names to their generated password values. - # This is the ideal format for a for_each loop, ensuring each resource - # instance has a unique, string-based key. - generated_secrets_map = { - for name, password_obj in random_password.generated_secret : name => password_obj.result - } - - # Create a list of objects from the map. - # This is useful for outputs that require a list format. - generated_secret_list = [ - for name, value in local.generated_secrets_map : { - name = name - value = value - } - ] -} \ No newline at end of file diff --git a/terraform/modules/generated_secrets/main.tf b/terraform/modules/generated_secrets/main.tf deleted file mode 100644 index d411dd6..0000000 --- a/terraform/modules/generated_secrets/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -terraform { - required_providers { - infisical = { - source = "infisical/infisical" - version = "0.15.19" - } - } -} - -data "infisical_projects" "home-net" { - slug = var.project_slug -} - -resource "random_password" "generated_secret" { - for_each = toset(var.name) - - length = 24 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - -resource "infisical_secret" "stored_secret" { - for_each = local.generated_secrets_map - - name = each.key - value = each.value - env_slug = "dev" - workspace_id = data.infisical_projects.home-net.id - folder_path = "/generated/credentials" -} \ No newline at end of file diff --git a/terraform/modules/generated_secrets/outputs.tf b/terraform/modules/generated_secrets/outputs.tf deleted file mode 100644 index 51f325b..0000000 --- a/terraform/modules/generated_secrets/outputs.tf +++ /dev/null @@ -1,11 +0,0 @@ -output "generated_secrets_list" { - description = "A list of objects, each containing the name and generated value of a secret." - value = local.generated_secret_list - sensitive = true -} - -output "generated_secrets_map" { - description = "A map of generated secret names to their values." - value = local.generated_secrets_map - sensitive = true -} \ No newline at end of file diff --git a/terraform/modules/generated_secrets/variables.tf b/terraform/modules/generated_secrets/variables.tf deleted file mode 100644 index ff41a85..0000000 --- a/terraform/modules/generated_secrets/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "name" { - type = list(string) - description = "List of secret names to generate" -} - -variable "project_slug" { - type = string - description = "Slug of project to store secret in" - default = "home-net-ln-sy" - -} \ No newline at end of file diff --git a/terraform/modules/oauth_auth/service.tf b/terraform/modules/oauth_auth/service.tf index c646008..57aa214 100644 --- a/terraform/modules/oauth_auth/service.tf +++ b/terraform/modules/oauth_auth/service.tf @@ -1,4 +1,4 @@ -resource "random_string" "example" { +resource "random_string" "client_id" { length = 16 special = false upper = true @@ -43,7 +43,7 @@ data "authentik_certificate_key_pair" "generated" { resource "authentik_provider_oauth2" "name" { name = lower(replace(var.name, " ", "-")) - client_id = random_string.example.result + client_id = random_string.client_id.result authorization_flow = data.authentik_flow.default-authorization-flow.id invalidation_flow = data.authentik_flow.default-invalidation-flow.id signing_key = data.authentik_certificate_key_pair.generated.id diff --git a/terraform/providers.tf b/terraform/providers.tf index 901c4ea..3652c4e 100644 --- a/terraform/providers.tf +++ b/terraform/providers.tf @@ -4,16 +4,16 @@ provider "nginxproxymanager" { password = var.nginx_proxy_pass } - provider "docker" { host = "unix:///var/run/docker.sock" +// host = "ssh://root@192.168.1.41:22" +// ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"] } /* provider "docker" { - host = "ssh://root@192.168.1.41:22" - ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"] -}*/ +} +*/ provider "technitium" { url = var.technitium_host diff --git a/terraform/services.tf b/terraform/services.tf index e0fc563..b58682e 100644 --- a/terraform/services.tf +++ b/terraform/services.tf @@ -1,31 +1,15 @@ -module "secrets" { - source = "./modules/generated_secrets" - - name = local.secrets -} - module "stacks" { for_each = local.stacks source = "./modules/docker-stack" stack = each.value system = local.system - - //depends_on = [ module.secrets ] } -module "flaresolverr_service" { - source = "./modules/docker" - - container_name = local.services.flaresolverr.service_name - container_image = local.services.flaresolverr.image_name - networks = local.services.flaresolverr.network.networks - environment_vars = local.services.flaresolverr.env -} +module "services"{ + for_each = local.services + source = "./modules/docker-service" -module "delugevpn_service" { - source = "./services/deluge-vpn" - service = local.services.deluge-vpn - vpn_pass = var.vpn_pass - vpn_user = var.vpn_user + service = each.value + system = local.system } \ No newline at end of file