Files
kportal/README.md
T
4d263319ab feat: add --context to forward only selected contexts (#85)
* feat: add --context to forward only selected contexts

kportal forwards every context in the config file. When the file describes
several clusters that host the same services, that means holding connections
open to clusters nobody is using, and - because all contexts are active at once
- every localPort has to be globally unique. The same service therefore lands on
a different local port per cluster, so switching clusters means reconfiguring
the app being developed.

--context limits a run to the contexts you name. It is repeatable and accepts
comma-separated values, and composes with -headless:

    kportal --context development-team-a
    kportal --context team-a,team-b -headless
    kportal --context team-a --context team-b

An unknown name is an error listing the contexts the file defines, so a typo
fails loudly instead of silently forwarding nothing.

Local-port and mDNS-hostname conflicts are now scoped to forwards that could run
at the same time: same context, or both contexts in the active selection. With
no selection every context is active, so existing behaviour is unchanged.
Contexts that are never selected together may reuse the same localPort, which is
what lets a service keep one fixed local port in every cluster.

Validation still covers the whole file, so a problem in a context outside the
selection is still reported and --check does not get weaker. Loading, validating
and filtering are shared by every path that re-reads the config (startup, file
watcher, SIGHUP in both headless and verbose modes), so no path can forget the
selection.

Also fixes a latent identity bug this makes reachable: Forward.ID() is only
"alias:localPort" when an alias is set, which was unique across contexts only
because duplicate local ports were globally rejected. With ports now shareable,
deleting or editing a forward from the TUI could have matched same-alias
forwards in other contexts and silently removed them. Delete and edit are now
scoped to the context the row came from.

* fix(config): keep the running config when no selected context remains

A reload dropped selected contexts that no longer exist in the file.
When all of them vanished, the reload returned an empty config and the
manager stopped every forward after a single rename.

LoadForRuntime now returns an error in that case. The watcher keeps the
previous config, as it does for any other invalid reload.

Refs #80

* fix(ui): apply new table columns after a successful SIGHUP reload

The context selection moved the SIGHUP reload into reloadFromSignal,
which had no way to update the verbose table columns. The function now
takes a callback that runs only after the manager reloads without error.
The verbose table passes SetColumns.

Refs #80

* style: gofmt run_test.go

---------

Co-authored-by: André Cruz <[email protected]>
2026-10-02 02:33:29 +01:00

19 KiB

kportal logo

Release License Go Report Card

Kubernetes port-forward manager with interactive terminal UI

kportal manages multiple Kubernetes port-forwards with an interactive terminal interface. It provides real-time status updates, automatic reconnection, hot-reload configuration, and mDNS hostname publishing.

kportal Screenshot

✨ Features

  • Interactive TUI - Terminal interface with keyboard navigation
  • Live management - Add, edit, and delete port-forwards without restarting
  • Auto-reconnect - Exponential backoff retry on connection failures
  • Hot-reload - Configuration changes applied automatically
  • Health monitoring - Multiple check methods with stale connection detection
  • Multi-context - Support for multiple Kubernetes contexts and namespaces
  • Pod restart handling - Automatic reconnection when pods restart
  • Label selectors - Dynamic pod targeting using label selectors
  • Port conflict detection - Validates port availability with PID information
  • mDNS hostnames - Access forwards via .local hostnames
  • HTTP traffic logging - Real-time HTTP request/response logging for debugging
  • Connection benchmarking - Built-in HTTP benchmarking with latency statistics
  • Headless mode - Background operation for scripting and automation

🔄 Comparison with Other Tools

Feature kportal k9s Kube Forwarder kftray
Interface Terminal TUI Terminal TUI Desktop GUI (Electron) Desktop GUI + TUI
Persistent Config ✅ YAML file ❌ Session only ✅ JSON bookmarks ✅ JSON + Git sync
Auto-reconnect ✅ Exponential backoff ❌ Manual ✅ Basic ✅ Watch API
Hot-reload Config ✅ File watch + SIGHUP ❌ ❌ ❌
Health Checks ✅ TCP + data-transfer ❌ ❌ ❌
Stale Connection Detection ✅ Age + idle tracking ❌ ❌ ❌
HTTP Traffic Logging ✅ Built-in viewer ❌ ❌ ✅
Connection Benchmarking ✅ Built-in ✅ Via Hey ❌ ❌
mDNS Hostnames ✅ .local domains ❌ ❌ ❌
Label Selectors ✅ ✅ ❌ ✅
Multi-context ✅ ✅ ✅ ✅
Headless Mode ✅ ❌ ❌ ❌
System Tray ❌ ❌ ❌ ✅
UDP Support ❌ ❌ ❌ ✅ Proxy relay
Dependencies Single binary Single binary Electron Tauri + kubectl

📦 Installation

Homebrew (macOS)

brew install --cask lukaszraczylo/taps/kportal

Note

: If you previously installed via brew install lukaszraczylo/taps/kportal (formula), uninstall first:

brew uninstall kportal

Quick Install

curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash

The installer downloads kportal-<version>-checksums.txt from the same release and verifies the archive's SHA-256 before installing. If cosign is on your PATH, the checksums file's keyless cosign signature is also verified against the shared-actions reusable workflow identity.

Variable Effect
DRY_RUN=1 Download and verify only; do not install
SKIP_COSIGN=1 Skip cosign signature verification (SHA-256 is still enforced)

Manual Download

Download binaries from the releases page.

Build from Source

git clone https://github.com/lukaszraczylo/kportal.git
cd kportal
make build && make install

Verifying Release Signatures

All release checksums are signed with cosign using keyless signing. To verify:

# Download the checksum file and its sigstore bundle from the release
cosign verify-blob \
  --certificate-identity-regexp "^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  --bundle "kportal-<version>-checksums.txt.sigstore.json" \
  kportal-<version>-checksums.txt

🚀 Quick Start

Create .kportal.yaml:

contexts:
  - name: production
    namespaces:
      - name: backend
        forwards:
          - resource: service/postgres
            protocol: tcp
            port: 5432
            localPort: 5432
            alias: prod-db

          - resource: service/api
            protocol: tcp
            port: 8080
            localPort: 8080
            alias: api
            httpLog: true  # Enable HTTP traffic logging

Run:

kportal

Keyboard Controls

Key Action
↑↓ / j/k Navigate
Space / Enter Toggle forward
n Add new forward
e Edit forward
d Delete forward
b Benchmark connection
l View HTTP logs
q Quit

📖 Configuration

Basic Structure

contexts:
  - name: <context-name>
    namespaces:
      - name: <namespace-name>
        forwards:
          - resource: <type>/<name>
            protocol: tcp
            port: <remote-port>
            localPort: <local-port>
            alias: <display-name>      # optional
            selector: <label-selector> # optional
            httpLog: true              # optional - enable HTTP logging

Forward Options

Field Required Description
resource Yes Resource type and name (e.g., service/postgres, pod/my-app)
protocol Yes Protocol (tcp)
port Yes Remote port
localPort Yes Local port
alias No Display name and mDNS hostname
selector No Label selector for pod resolution
httpLog No Enable HTTP traffic logging (true/false)

Resource Formats

Format Description
service/name Service forwarding
pod/name Direct pod by name
pod/prefix Pod by prefix (matches prefix-*)
pod + selector Pod by label selector
deployment/name Deployment

Health Check Configuration

healthCheck:
  interval: "3s"           # Check frequency
  timeout: "2s"            # Check timeout
  method: "data-transfer"  # tcp-dial or data-transfer
  maxConnectionAge: "25m"  # Reconnect before k8s timeout
  maxIdleTime: "10m"       # Detect idle connections

reliability:
  tcpKeepalive: "30s"
  dialTimeout: "30s"
  retryOnStale: true

Health check methods:

  • tcp-dial - Fast TCP connection test
  • data-transfer - Verifies tunnel functionality by attempting data read

Connection age reconnection only triggers when the connection is also idle, preventing interruption of active transfers like database dumps.

mDNS Hostnames

Enable mDNS to access forwards via .local hostnames:

mdns:
  enabled: true

contexts:
  - name: production
    namespaces:
      - name: default
        forwards:
          - resource: service/postgres
            port: 5432
            localPort: 5432
            alias: prod-db  # Accessible via prod-db.local:5432
  • Explicit alias becomes <alias>.local
  • Without alias, hostname is generated from resource name (service/redis → redis.local)
  • Works on macOS (Bonjour) and Linux (avahi-daemon)

Verify registration:

dns-sd -B _kportal._tcp local       # macOS
avahi-browse -t _kportal._tcp       # Linux

Table Columns

Choose which columns the forwards table shows, and in what order. The setting applies to the interactive UI and the -verbose table:

tui:
  columns:
    - name: alias
      width: 30      # optional, 1-200
    - name: resource
    - name: local
    - name: status
  • Valid names: context, namespace, alias, type, resource, remote, local, status
  • The list order is the render order, and unlisted columns are hidden
  • width is the maximum text width, from 1 to 200. If you omit it, the column keeps its default width
  • If you omit tui, the table keeps its default columns and order
  • Changes apply on hot-reload

Usage

Interactive Mode

kportal

Verbose Mode

kportal -v

Headless Mode

Run without TUI for scripting and automation:

kportal -headless

Headless mode emits both structured and standard-library logs to stderr by default (suitable for redirecting to a log file or systemd journal). The -v flag controls log level (debug vs info), not destination.

Combines well with verbose mode for background operation:

kportal -headless -v 2>kportal.log &

Validate Configuration

kportal --check

Custom Config File

kportal -c /path/to/config.yaml

Select Contexts

By default every context in the config file is forwarded. --context limits the run to the contexts you name, which is useful when the file describes several clusters but you only work against one at a time:

kportal --context development-team-a

The flag is repeatable and accepts comma-separated values, so these are equivalent:

kportal --context team-a --context team-b
kportal --context team-a,team-b

It composes with the other modes, and works well as a per-cluster shell alias:

alias kp-a='kportal --context development-team-a'
kportal --context development-team-a -headless

An unknown context name is an error that lists the contexts the file defines, so a typo fails loudly instead of silently forwarding nothing.

Reusing local ports across contexts

Local-port conflicts are only reported between forwards that could run at the same time. Contexts that are never selected together may therefore reuse the same localPort, so the same service keeps the same local port in every cluster and the app you are developing never needs reconfiguring:

contexts:
  - name: development-team-a
    namespaces:
      - name: rate-service
        forwards:
          - &rate-service
            resource: pod/rpc-server
            protocol: tcp
            port: 50051
            localPort: 3004
            alias: rate-service

  - name: development-team-b
    namespaces:
      - name: rate-service
        forwards:
          - <<: *rate-service   # same local port, no conflict

This requires passing --context. Without it every context is active, so the duplicate ports above would genuinely collide and are still reported as an error - as is naming both contexts at once (--context team-a,team-b).

Generate Forwards from a Cluster

The generate subcommand discovers services in a Kubernetes context and lets you interactively pick which ones to forward. Selected entries are appended to the config file with consecutive local ports starting from a value you choose.

kportal generate --context=my-cluster
kportal generate --context=my-cluster --config=/path/to/.kportal.yaml
kportal generate --context=my-cluster --dry-run
Flag Description
--context (required) Kubernetes context to scan
--config Path to kportal config file (default: .kportal.yaml)
--dry-run Print the planned forwards but do not modify the config

The interactive flow has three steps:

  1. Namespaces — multi-select with space, toggle-all with a, filter with /.
  2. Services — same controls; rows already present in the config are locked off, and non-TCP ports are skipped (UDP is not supported by kportal's forward layer).
  3. Port assignment — choose a starting local port (default 10000, must be ≥ 1024). Local ports are assigned consecutively in stable order, skipping any already in use.

Press enter on the final step to save (or to print and exit when --dry-run is set), b to go back, or esc to cancel.

Status Indicators

Indicator Description
● Active Connection healthy
○ Starting Initial connection (10s grace period)
◐ Reconnecting Reconnecting after failure
✗ Error Connection failed
○ Disabled Manually disabled

Advanced Features

HTTP Traffic Logging

Press l in the TUI to view real-time HTTP traffic for a selected forward. The log viewer shows:

Column Description
TIME Request timestamp
METHOD HTTP method (GET, POST, etc.)
STATUS Response status code
LATENCY Request duration
PATH Request path

List view shortcuts:

Key Action
↑/↓ Navigate entries
Enter View request details
g/G Jump to top/bottom
a Toggle auto-scroll
f Cycle filter mode (All → Non-2xx → Errors)
/ Search by path or method
c Clear all filters
q Close log viewer

Detail view:

Press Enter on any entry to see full request/response details including:

  • Request and response headers (alphabetically sorted)
  • Request and response bodies
  • Timing information and status codes
Key Action
↑/↓ Scroll content
PgUp/PgDn Scroll by page
g Jump to top
c Copy response body to clipboard
Esc/q Return to list

Body display features:

  • JSON formatting - JSON bodies are pretty-printed with syntax highlighting
  • Compression handling - gzip/deflate content is automatically decompressed
  • Binary detection - Binary content shows a placeholder instead of garbled data

Filter modes:

  • All - Show all entries
  • Non-2xx - Hide successful (2xx) responses
  • Errors - Show only 4xx and 5xx responses

Toggling per-forward logging:

In the add/edit wizard, press h on the confirmation step to toggle httpLog on or off for the current forward. The wizard preserves any advanced httpLog keys (logFile, includeHeaders, maxBodySize, filterPath) you set in YAML.

Header redaction:

When httpLog.includeHeaders: true is set, sensitive header values are automatically replaced with [REDACTED]. The header name is preserved so you can see that an Authorization header was present without exposing its value. Redacted headers include Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token, Proxy-Authorization, X-Access-Token, and any header whose name contains token, secret, password, or apikey. This is always on and cannot be disabled.

Advanced configuration:

forwards:
  - resource: service/api
    port: 8080
    localPort: 8080
    httpLog:
      enabled: true
      includeHeaders: true   # values of sensitive headers are redacted
      maxBodySize: 65536     # bytes; 0 = unlimited
      filterPath: "/api/"    # only log paths matching this substring
      logFile: "api.log"     # append entries to a file in addition to the in-memory ring

Connection Benchmarking

Press b in the TUI to benchmark a selected forward. Configure:

  • URL Path - Target endpoint (default: /)
  • Method - HTTP method (GET, POST, etc.)
  • Concurrency - Number of parallel workers
  • Requests - Total number of requests

Results include:

  • Success/failure counts
  • Min/Max/Avg latency
  • P50/P95/P99 percentiles
  • Throughput (requests/sec)
  • Status code distribution

Hot-Reload

Configuration changes are applied automatically. Manual reload:

kill -HUP $(pgrep kportal)

Port Conflict Detection

kportal validates port availability at startup and during hot-reload, showing which process is using conflicting ports.

Conflicts are only reported between forwards that could be running together. With --context, forwards in contexts outside the selection are ignored, which is what lets each context reuse the same localPort values - see Reusing local ports across contexts.

Retry Strategy

Exponential backoff: 1s → 2s → 4s → 8s → 10s (max). Retries continue indefinitely until connection succeeds.

Migration from kftray

kportal --convert configs.json --convert-output .kportal.yaml

Signal Handling

  • Ctrl+C / SIGTERM - Graceful shutdown
  • SIGHUP - Reload configuration

🐛 Troubleshooting

Port Already in Use

lsof -i :<port>
kill <pid>

Connection Refused

  1. Verify pod is running: kubectl get pods -n <namespace>
  2. Verify port is correct: kubectl describe pod <pod>
  3. Check service endpoints: kubectl get endpoints <service>

Context Not Found

kubectl config get-contexts

Context names containing @, ., :, or / (e.g. admin@home, [email protected], GKE dotted names, EKS ARNs) are accepted by the config validator.

If --context reports unknown context "...", the name does not match any contexts[].name in the config file. The error lists the names that are defined - note these are the contexts in your .kportal.yaml, which need not be every context in your kubeconfig.

🔧 Development

Prerequisites

  • Go 1.23+
  • Kubernetes cluster access
  • kubectl configured

Build

make build    # Build binary
make test     # Run tests
make all      # fmt, vet, staticcheck, test
make install  # Install locally

Contributing

See CONTRIBUTING.md for guidelines.

Telemetry

On startup this binary sends a single anonymous adoption ping — project name, version, timestamp; no identifiers, no command output, no payload contents. Fire-and-forget with a 2-second timeout; cannot block startup or panic.

See oss-telemetry — Disabling telemetry for the exact wire format, source, and full opt-out documentation.

Quick opt-out: set any of DO_NOT_TRACK=1, OSS_TELEMETRY_DISABLED=1, or KPORTAL_DISABLE_TELEMETRY=1.

License

MIT License - see LICENSE.

Acknowledgments