Network configuration

This guide covers network configuration for Astro Private Cloud (APC), including ingress, proxy settings, private networking, and network policies.

Architecture

APC uses NGINX as its default ingress controller. For TLS certificate configuration, see TLS certificate management. To use a pre-existing ingress controller, see Use a third-party ingress controller.

DNS requirements

Before configuring ingress, create a wildcard DNS record pointing to the load balancer IP or hostname that NGINX will provision:

*.<baseDomain> → <load-balancer-IP-or-hostname>

APC creates the following endpoints from global.baseDomain. All endpoints are served over HTTPS and covered by a single wildcard TLS certificate for *.<baseDomain>.

Control plane endpoints

EndpointPurposeAccessible from
app.<baseDomain>Astro UI — primary user interfacePublic
houston.<baseDomain>Houston API (GraphQL/REST)Public
registry.<baseDomain>Container image registryPublic
grafana.<baseDomain>Grafana dashboardsPublic
prometheus.<baseDomain>Prometheus UIPublic
alertmanager.<baseDomain>Alertmanager UIPublic

Airflow Deployment endpoints

Each Airflow Deployment gets a path-based URL under the deployments subdomain:

URL patternPurpose
https://deployments.<baseDomain>/<release-name>/airflowAirflow UI
https://deployments.<baseDomain>/<release-name>/flower/Celery Flower UI (Celery executor only)

Data plane endpoints

In a split-plane deployment, both planes share the same global.baseDomain. The data plane uses global.plane.domainPrefix (a unique cluster identifier, for example dp01) to scope its endpoints under <domainPrefix>.<baseDomain>:

1# Control plane values.yaml
2global:
3 baseDomain: localtest.me
4 plane:
5 mode: control
6
7# Data plane values.yaml
8global:
9 baseDomain: localtest.me # Must match control plane
10 plane:
11 mode: data
12 domainPrefix: dp01 # Unique identifier for this data plane cluster

Both planes must use the same global.baseDomain. Auth tokens issued by the control plane Houston are scoped to .<baseDomain>, so a mismatched base domain causes authentication failures across planes.

Create a separate wildcard DNS record for the data plane:

*.<domainPrefix>.<baseDomain> → <data-plane-load-balancer>

The data plane creates the following endpoints:

EndpointPurpose
registry.<domainPrefix>.<baseDomain>Container image registry
commander.<domainPrefix>.<baseDomain>Commander gRPC — used by the control plane to manage Airflow Deployments on this data plane
<domainPrefix>.<baseDomain>/metadataNamespace label metadata served by Commander (read by the control plane)
prometheus.<domainPrefix>.<baseDomain>Prometheus UI
prom-proxy.<domainPrefix>.<baseDomain>Prometheus federation auth proxy — the control plane scrapes this endpoint to aggregate metrics
elasticsearch.<domainPrefix>.<baseDomain>Elasticsearch
deployments.<domainPrefix>.<baseDomain>/<release-name>/airflowAirflow UI for each Deployment on this data plane

Metadata endpoint

The /metadata path on <domainPrefix>.<baseDomain> is served by Commander and returns a JSON document. The control plane reads this endpoint to fetch data plane details, including:

  • Kubernetes version and health status
  • Commander and registry URLs
  • Data plane chart version, cloud provider, and region
  • Namespace pools configuration and namespace labels
  • Elasticsearch configuration
  • External secret manager configuration

This endpoint is required for the data plane to function correctly with the control plane.

Retrieve your load balancer IP or hostname after installing APC. For the data plane, run kubectl get svc -n <astronomer-namespace> -l component=dp-ingress-controller. For the control plane, run kubectl get svc -n <astronomer-namespace> -l component=cp-ingress-controller. Create the wildcard DNS record pointing to that address before users try to access the platform.

Ingress configuration

Load balancer

To expose the platform through a cloud load balancer, add the following to your values.yaml:

1nginx:
2 serviceType: LoadBalancer
3 loadBalancerIP: "" # Optional: specific IP
4 loadBalancerSourceRanges:
5 - 10.0.0.0/8
6 - 192.168.0.0/16

Private load balancer

To provision an internal (non-public) load balancer, set privateLoadBalancer: true. APC automatically applies the appropriate annotation for AWS, GCP, and Azure:

1nginx:
2 privateLoadBalancer: true

To add custom annotations to the ingress service beyond the auto-applied cloud annotations, use ingressAnnotations:

1nginx:
2 privateLoadBalancer: true
3 ingressAnnotations:
4 service.beta.kubernetes.io/aws-load-balancer-subnets: "subnet-abc123"

Custom ingress annotations

global.extraAnnotations applies annotations to all APC ingress resources — the platform UI, Houston API, registry, Grafana, Prometheus, Alertmanager, and Elasticsearch ingresses. Its behavior depends on whether you’re using the auth sidecar or the default NGINX ingress controller.

With global.authSidecar.enabled: true

All annotations under global.extraAnnotations are applied to every platform and Airflow ingress object, giving full control for environments using a bring-your-own ingress controller:

1global:
2 authSidecar:
3 enabled: true
4 extraAnnotations:
5 kubernetes.io/ingress.class: "my-ingress-controller"

For OpenShift route passthrough TLS:

1global:
2 extraAnnotations:
3 route.openshift.io/termination: "passthrough" # Requires authSidecar.enabled: true

With default NGINX ingress

global.extraAnnotations is still respected, but you can’t override annotations that APC manages. Use this to add custom annotations globally without conflicting with platform defaults.

The following annotations are protected and can’t be overridden via global.extraAnnotations:

  • kubernetes.io/ingress.class
  • nginx.ingress.kubernetes.io/custom-http-errors

global.extraAnnotations applies to all ingress resources cluster-wide. nginx.ingressAnnotations applies only to the NGINX load balancer service. Use global.extraAnnotations for ingress routing behavior and nginx.ingressAnnotations for cloud load balancer configuration.

NodePort

1nginx:
2 serviceType: NodePort
3 httpNodePort: 30080
4 httpsNodePort: 30443

Source IP preservation

By default, nginx.preserveSourceIP: false sets externalTrafficPolicy: Cluster on the NGINX service, which distributes traffic evenly across all nodes but replaces the original client IP with the node IP through SNAT.

Set preserveSourceIP: true to use externalTrafficPolicy: Local, which preserves the original client IP in Airflow logs and Astro UI audit logs:

1nginx:
2 preserveSourceIP: true

With externalTrafficPolicy: Local, traffic is only routed to nodes running an NGINX pod. This can cause uneven load distribution if NGINX pods aren’t spread across all nodes.

NGINX request limits

Adjust these values for environments with slow upstreams, large Dag bundles, or long-running API calls:

1nginx:
2 proxyConnectTimeout: 15 # seconds to establish an upstream connection
3 proxyReadTimeout: 600 # seconds to wait for a response from upstream
4 proxySendTimeout: 600 # seconds to transmit a request to upstream
5 proxyBodySize: "1024m" # maximum allowed request body size

The default proxyBodySize of 1024m limits Dag bundle upload sizes. Increase this value if you see 413 Request Entity Too Large errors.

Connect to public endpoints

Egress configuration

By default, Airflow Deployments can reach public endpoints. When network policies are enabled, you can restrict outbound traffic using Kubernetes NetworkPolicy resources. See Network policies.

Configure a proxy

APC’s Houston API reads proxy settings from environment variables on the Houston pod. Configure proxy settings using the houston.env Helm value in the astronomer subchart:

1astronomer:
2 houston:
3 env:
4 - name: HTTPS_PROXY
5 value: "http://proxy.example.com:8080"
6 - name: HTTP_PROXY
7 value: "http://proxy.example.com:8080"
8 - name: NO_PROXY
9 value: "localhost,127.0.0.1,kubernetes.default.svc,.svc.cluster.local,.<baseDomain>"

Houston reads the proxy URL from the following environment variables, checked in order of precedence:

  1. HTTPS_PROXY / https_proxy / GLOBAL_AGENT_HTTPS_PROXY — for HTTPS requests
  2. HTTP_PROXY / http_proxy / GLOBAL_AGENT_HTTP_PROXY — for HTTP requests

NO_PROXY is respected automatically by the proxy agent. Include .svc.cluster.local to prevent internal Kubernetes service traffic from routing through the proxy, kubernetes.default.svc to ensure Commander can reach the Kubernetes API server without proxying, and .<baseDomain> if any platform components resolve each other using external DNS rather than cluster-internal DNS.

The GLOBAL_AGENT_* variants apply to HTTP libraries that use global-agent for proxy bootstrapping, in addition to the standard HTTP_PROXY and HTTPS_PROXY variables used by axios.

In a proxy environment, apply the same environment variables to every platform pod that makes outbound calls. Set the same variables on Commander and Astro UI using astronomer.commander.env and astronomer.astroUI.env respectively.

To verify that Houston is using the proxy, check the Houston pod logs for the following line:

Configuring Axios to use proxy: http://proxy.example.com:8080

Configure proxy for Airflow Deployments

Configure proxy settings for Airflow tasks by setting environment variables on each Airflow Deployment:

1env:
2 - name: HTTP_PROXY
3 value: "http://proxy.example.com:8080"
4 - name: HTTPS_PROXY
5 value: "http://proxy.example.com:8080"
6 - name: NO_PROXY
7 value: "localhost,127.0.0.1,.svc.cluster.local,.<baseDomain>"

Private networking

VPC/private subnet access

To resolve private hostnames from pods, add hostAliases to the relevant component. Configuration differs between platform components and Airflow Deployment components.

Platform components

The following platform components support hostAliases: Prometheus, registry, Houston, Houston worker, and Commander:

1prometheus:
2 hostAliases:
3 - ip: "10.0.0.100"
4 hostnames:
5 - "database.internal"
6 - "api.internal"
7astronomer:
8 registry:
9 hostAliases:
10 - ip: "10.0.0.100"
11 hostnames:
12 - "database.internal"
13 - "api.internal"
14 commander:
15 hostAliases:
16 - ip: "10.0.0.100"
17 hostnames:
18 - "database.internal"
19 - "api.internal"
20 houston:
21 hostAliases:
22 - ip: "10.0.0.100"
23 hostnames:
24 - "database.internal"
25 - "api.internal"
26 worker:
27 hostAliases:
28 - ip: "10.0.0.100"
29 hostnames:
30 - "database.internal"
31 - "api.internal"

Airflow Deployment components

The following Airflow components support hostAliases: scheduler, API server, webserver, triggerer, and workers:

1airflow:
2 scheduler:
3 hostAliases: []
4 apiServer:
5 hostAliases: []
6 webserver:
7 hostAliases: []
8 triggerer:
9 hostAliases: []
10 workers:
11 hostAliases: []

VPN/direct connect

For AWS PrivateLink, Azure Private Link, or GCP Private Service Connect:

  1. Configure the private endpoint in your cloud provider.
  2. Create Kubernetes Service and Endpoints resources pointing to the private IP.
  3. Reference the service name in your Airflow connections.
1apiVersion: v1
2kind: Endpoints
3metadata:
4 name: private-database
5subsets:
6 - addresses:
7 - ip: 10.0.0.100
8 ports:
9 - port: 5432
10---
11apiVersion: v1
12kind: Service
13metadata:
14 name: private-database
15spec:
16 ports:
17 - port: 5432

Service mesh integration

Istio

APC is compatible with Istio, but APC doesn’t configure or manage Istio. You must install and configure Istio on your cluster yourself before enabling this feature in APC.

To enable Istio compatibility mode:

1global:
2 istio:
3 enabled: true
4 rootNamespace: "istio-config" # Must match your Istio installation's root namespace

The rootNamespace must match the root config namespace of your Istio installation. In most Istio installations this is istio-system or istio-config. If this value is incorrect, the default Sidecar egress policy won’t apply to Airflow Deployment namespaces.

APC uses the networking.istio.io/v1alpha3 API for Sidecar resources. This API is deprecated in Istio 1.22 and later. Clusters running Istio 1.22+ will see deprecation warnings in the Istio control plane logs.

What APC creates when Istio is enabled

APC creates three Istio Sidecar resources to control egress traffic:

ResourceNamespaceApplies toAllowed egress
default-sidecar-configrootNamespaceAll Airflow Deployment namespaces (default)Same namespace, istio-system, Elasticsearch, SQL proxy
<release-namespace>-sidecar-configPlatform namespaceAll platform podsSame namespace, istio-system, kube-system
<release-namespace>-prometheus-sidecar-configPlatform namespacePrometheus onlyEverywhere (*/*) — required for metrics scraping

The default-sidecar-config resource in the rootNamespace acts as a cluster-wide default for any namespace that doesn’t have its own Sidecar resource, which covers all Airflow Deployment namespaces.

Namespace labeling for sidecar injection

APC doesn’t automatically label namespaces for Istio sidecar injection. You must label the platform namespace and any Airflow Deployment namespaces yourself for injection to take effect on long-running pods (Houston, Commander, Astro UI, Registry):

$kubectl label namespace <astronomer-namespace> istio-injection=enabled
$kubectl label namespace <airflow-namespace> istio-injection=enabled

If you use namespace pools, apply the label automatically to all Airflow namespaces using global.namespaceLabels:

1global:
2 namespaceLabels:
3 istio-injection: "enabled"

Airflow Deployment egress under Istio

The default-sidecar-config restricts Airflow pod egress to the same namespace, istio-system, Elasticsearch, and SQL proxy only. Dag tasks that connect to external databases or APIs through the service mesh will be blocked.

To allow additional egress for a specific Airflow namespace, create a custom Sidecar resource in that namespace that overrides the default:

1apiVersion: networking.istio.io/v1alpha3
2kind: Sidecar
3metadata:
4 name: allow-external-db
5 namespace: <airflow-namespace>
6spec:
7 egress:
8 - hosts:
9 - "./*"
10 - "istio-system/*"
11 - "*/my-external-database.example.com"

Istio Sidecar resources only restrict traffic to services registered in the mesh. Egress to external public IPs not registered as Kubernetes services remains reachable by default unless you set outboundTrafficPolicy: REGISTRY_ONLY on the mesh.

Sidecar injection behavior

APC disables Istio sidecar injection on pods that shouldn’t have a proxy:

  • NGINX (control plane and data plane) — injection disabled; also has traffic.sidecar.istio.io/includeInboundPorts: "" set unconditionally on all NGINX pods to prevent accidental inbound port interception even if Istio is installed but not yet enabled in values
  • Houston cronjobs and Helm hooks — injection disabled; short-lived batch jobs don’t need a sidecar proxy.
  • Config syncer, Commander JWKS hook, NATS JetStream job — injection disabled for the same reason

Prometheus is the only platform component that receives the Istio sidecar, with explicit resource allocation:

1sidecar.istio.io/proxyCPU: "500m"
2sidecar.istio.io/proxyMemory: "400Mi"

What APC doesn’t configure

APC doesn’t create VirtualServices, DestinationRules, Gateways, AuthorizationPolicy, or PeerAuthentication resources. Configure mTLS mode and traffic management policies directly in Istio. See the Istio mTLS documentation for details.

Network policies

Enable network policies

1global:
2 networkPolicy:
3 enabled: true
4 defaultDenyNetworkPolicy: true # default: true

When both networkPolicy.enabled and defaultDenyNetworkPolicy are true, APC creates a NetworkPolicy named <release-name>-default-deny-ingress in the platform namespace that denies all inbound traffic by default. Per-component network policies then selectively allow the required connections.

The default-deny policy covers ingress only. Egress from platform and Airflow pods is unrestricted by default. Apply custom NetworkPolicy resources to restrict outbound traffic.

Set defaultDenyNetworkPolicy: false if your cluster already has a deny-all policy in place, or if other services running in the platform namespace would be disrupted.

Default policies

When enabled, APC creates network policies for:

  • Platform component communication
  • Airflow Deployment isolation
  • Ingress traffic

APC’s Houston network policy allows ingress from any pod labeled tier: airflow across all namespaces, using namespaceSelector: {}. Isolation between Airflow Deployments is enforced through Kubernetes RBAC, not network policies.

Custom policies

Apply custom Kubernetes NetworkPolicy resources to extend or restrict traffic beyond the default APC-managed policies.

Allow specific egress

1apiVersion: networking.k8s.io/v1
2kind: NetworkPolicy
3metadata:
4 name: allow-database
5 namespace: deployment-namespace
6spec:
7 podSelector: {}
8 policyTypes:
9 - Egress
10 egress:
11 - to:
12 - ipBlock:
13 cidr: 10.0.0.0/24
14 ports:
15 - protocol: TCP
16 port: 5432

Restrict cross-namespace

1apiVersion: networking.k8s.io/v1
2kind: NetworkPolicy
3metadata:
4 name: deny-cross-namespace
5spec:
6 podSelector: {}
7 policyTypes:
8 - Ingress
9 ingress:
10 - from:
11 - podSelector: {}

Namespace labeling

Any namespace management requires cluster-scoped permissions. These settings only take effect if APC is granted a cluster role to manage namespace resources.

global.networkNSLabels

Set global.networkNSLabels: true to label the platform namespace with platform: <release-name>. This also adds namespace label selectors to network policies, restricting traffic to pods in namespaces that carry the matching label.

1global:
2 networkNSLabels: true

For example, if APC is installed with release name apc-private-cloud in namespace astro-cloud, enabling this feature patches the namespace with platform=apc-private-cloud, allowing you to filter it with:

$kubectl get ns -l platform=apc-private-cloud

global.namespaceLabels

global.namespaceLabels applies custom labels to Airflow Deployment namespaces. Its behavior depends on who owns namespace provisioning:

With customer-managed namespaces (namespace pools): When customers create and own their namespaces, APC no longer has access to patch them and skips namespace label updates. Customers are responsible for applying the required labels to their own namespaces.

With Astronomer-owned provisioning (cluster role): APC manages namespace creation and applies both its own required labels and any labels specified under global.namespaceLabels:

1global:
2 networkNSLabels: true
3 namespaceLabels:
4 platform: "<release-name>"

TLS scope

global.ssl.enabled controls TLS for database connections only — the connection from platform components to PostgreSQL. It doesn’t enable mTLS between platform services or TLS on internal cluster communication.

SettingScope
global.ssl.enabled: trueDatabase connection TLS (platform → PostgreSQL)
Ingress TLSHandled by global.tlsSecret and the NGINX ingress controller

Don’t rely on global.ssl.enabled to secure inter-service traffic. APC doesn’t configure Istio — if you need service mesh mTLS, install and configure Istio directly in your cluster.

DNS and service discovery

Internal DNS

Airflow Deployments use Kubernetes DNS for internal service resolution:

  • Services: <service>.<namespace>.svc.cluster.local
  • Pods: <pod-ip>.<namespace>.pod.cluster.local

Firewall requirements

Ingress ports

PortProtocolPurpose
443HTTPSPlatform UI/API
80HTTPRedirect to HTTPS

Egress requirements

DestinationPortPurpose
Container registry443Image pulls
PostgreSQL5432Database
Redis6379Celery broker
Your data sourcesVariousDag connections

Control plane and data plane connectivity

In a split-plane deployment, the control plane reaches the data plane over HTTPS, and the data plane reaches the control plane Houston API:

SourceDestinationPortPurpose
Control planecommander.<domainPrefix>.<baseDomain>443Control plane manages Airflow Deployments on the data plane
Control planeprom-proxy.<domainPrefix>.<baseDomain>443Control plane scrapes metrics from the data plane
Data planehouston.<baseDomain>443Data plane registers and communicates with the control plane

Ensure firewall rules between the two cluster load balancers permit outbound HTTPS on port 443 in both directions.

Internal cluster ports

PortPurpose
8871Houston API
8880Commander (HTTP)
50051Commander (gRPC)
443Kubernetes API server (required by Commander)
8080Airflow webserver
5555Flower
6379Redis
9200Elasticsearch (HTTP)
9300Elasticsearch (transport)
4222NATS (messaging)
5000Container registry

Troubleshoot

Connectivity issues

$# Test DNS resolution
$kubectl exec -n <namespace> <pod> -- nslookup api.example.com
$
$# Test connectivity
$kubectl exec -n <namespace> <pod> -- curl -v https://api.example.com
$
$# Check network policies
$kubectl get networkpolicies -n <namespace>

Proxy issues

$# Verify proxy settings on the Houston pod
$kubectl exec -n <astronomer-namespace> <houston-pod> -- env | grep -i proxy
$
$# Verify proxy settings on an Airflow Deployment pod
$kubectl exec -n <deployment-namespace> <pod> -- env | grep -i proxy
$
$# Test connectivity through proxy
$kubectl exec -n <namespace> <pod> -- curl -v --proxy http://proxy:8080 https://api.example.com

DNS issues

$# Check CoreDNS
$kubectl logs -n kube-system -l k8s-app=kube-dns
$
$# Test internal DNS resolution
$kubectl exec -n <namespace> <pod> -- nslookup houston.<astronomer-namespace>.svc.cluster.local

Best practices

  • Enable network policies to enforce least-privilege access.
  • Include .svc.cluster.local and kubernetes.default.svc in NO_PROXY to prevent internal Kubernetes traffic from routing through a proxy.
  • Use private load balancers for internal-only access.
  • Document all egress requirements for firewall teams.
  • Test connectivity before deploying Dags.
  • Use Kubernetes services for internal connections.