Kubernetes port-forward manager

Run all your Kubernetes port-forwards from one YAML file

kportal starts every forward listed in .kportal.yaml, shows live status in a terminal UI, reconnects dropped forwards and reloads the file when it changes. It also runs without the UI.

Homebrew, macOS
brew install --cask lukaszraczylo/taps/kportal

Linux, Windows and other install methods: Install.

Licence
MIT
Language
Go
Platforms
Linux, macOS, Windows
Architectures
amd64, arm64
kportal v0.11.2 - Port Forwarding Status

ALIAS         RESOURCE          REMOTE  LOCAL  STATUS
> prod-api     service/api         8080   8080  ● Active        
  prod-db      service/postgres    5432   5432  ● Active
  prometheus   pod                 9090   9090  ○ Starting
  staging-http pod/myapp           8080   8081  ◐ Reconnecting
  staging-met… pod/myapp           9090   9091  ● Active

↑↓/jk: Navigate  PgUp/Dn: Page  Space: Toggle  n: New  e: Edit
d: Delete  b: Bench  l: Logs  q: Quit

Example data. Column set from tui.columns: alias, resource, remote, local, status. Status strings and key bar match the source.

Interface

One table for every forward

Each row is one forward. Move with the arrow keys or j and k, toggle with Space, and add, edit or delete forwards without a restart.

kportal terminal UI with a table of ten port-forwards in the home context, all showing the Active status
Screenshot from kportal v0.2.17. The current key bar also lists PgUp/PgDn, b (Bench) and l (Logs).

Features

What kportal does

Every snippet below comes from the repository: .kportal.yaml, the UI source or the install script.

Configuration

Many forwards, one file

Group forwards by context and namespace. Each forward names a resource, a remote port and a local port.

  • service/<name> forwards to a service.
  • pod/<name> matches an exact pod name or a name prefix.
  • pod with selector picks a pod by label.
  • protocol is optional. Only tcp is accepted.
.kportal.yaml
contexts:
  - name: production
    namespaces:
      - name: default
        forwards:
          - resource: service/api
            protocol: tcp
            port: 8080
            localPort: 8080
            alias: prod-api
            httpLog: true

          - resource: service/postgres
            protocol: tcp
            port: 5432
            localPort: 5432
            alias: prod-db

      - name: monitoring
        forwards:
          - resource: pod
            selector: app=prometheus
            protocol: tcp
            port: 9090
            localPort: 9090
            alias: prometheus

  - name: staging
    namespaces:
      - name: default
        forwards:
          - resource: pod/myapp   # prefix match survives pod restarts
            protocol: tcp
            port: 8080
            localPort: 8081
            alias: staging-http

Terminal UI

Add, edit and delete forwards in the UI

Press n to add, e to edit, d to delete and Space to toggle a forward. The add wizard writes the change to the config file.

If the cluster does not let you list namespaces, the wizard asks you to type the namespace instead.

On the confirmation step, h toggles httpLog. Advanced httpLog keys that you set in YAML stay unchanged.

Enter Namespace:

Could not list namespaces (<error from the cluster>).
Type the namespace name instead.

Namespace: backend█

Text from internal/ui/wizard_views.go. The error in parentheses comes from the cluster.

Terminal UI

Choose the table columns

tui.columns sets which columns the table shows, their order and their width. List order is render order. Unlisted columns are hidden.

Valid names: context, namespace, alias, type, resource, remote, local, status. width is the maximum text width, 1 to 200.

The setting applies to the UI and to the -v table, and it reloads with the file.

.kportal.yaml
tui:
  columns:
    - name: alias
      width: 30      # optional, 1-200
    - name: resource
    - name: local
    - name: status

Reliability

Reconnect with backoff

When a forward drops, kportal retries with exponential backoff. The delay starts at 1 second, doubles up to a 10 second cap and carries 10% jitter. Retries do not stop.

Forwards by pod prefix or label selector connect to the new pod after a restart.

attempt   delay (plus or minus 10% jitter)
1         1s
2         2s
3         4s
4         8s
5+        10s   (cap)

Reliability

Health checks find stale connections

tcp-dial opens a TCP connection to the local port. data-transfer also tries to read data through the tunnel, and is the default.

A connection older than maxConnectionAge reconnects only when it is also idle, so a running transfer is not cut. A connection idle longer than maxIdleTime counts as stale.

The values shown are the defaults.

.kportal.yaml
healthCheck:
  interval: "3s"
  timeout: "2s"
  method: "data-transfer"  # or tcp-dial
  maxConnectionAge: "25m"
  maxIdleTime: "10m"

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

Operations

Hot reload and port-conflict checks

kportal reloads when the config file changes. SIGHUP reloads by hand. New forwards start, removed forwards stop and unchanged forwards keep running.

An invalid config is rejected and the previous one stays active. A reload that would bind a busy local port is rejected too.

The conflict report names the process that holds the port (found with lsof on Linux and macOS, tasklist on Windows).

manual reload
kill -HUP $(pgrep kportal)
Configuration diff: 1 to add, 0 to remove, 4 to keep

Config change rejected due to port conflicts:
  ...
Keeping previous configuration active

Contexts

Forward only the contexts you name

By default kportal forwards every context in the file. --context limits the run. Repeat the flag or separate names with commas.

Port conflicts are only reported between forwards that can run together. Contexts that you never select together can reuse a localPort.

An unknown name is an error that lists the contexts in the file. Those are the contexts[].name values, not every context in your kubeconfig.

shell
kportal --context development-team-a
kportal --context team-a --context team-b
kportal --context team-a,team-b
.kportal.yaml: same local port in two clusters
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

With no --context, or with both contexts named, the duplicate port is an error.

Bulk setup

Generate forwards from a cluster

kportal generate lists the services in a context and appends the ones you pick to the config file.

  1. Select namespaces: Space toggles, a toggles all, / filters.
  2. Select services. Services already in the config are locked and non-TCP ports are skipped.
  3. Pick the first local port. The default is 10000 and the minimum is 1024. Ports are assigned in order and skip ports in use.

Enter on the last step saves. b goes back and Esc cancels.

shell
kportal generate --context=my-cluster
kportal generate --context=my-cluster --config=/path/to/.kportal.yaml
kportal generate --context=my-cluster --dry-run
[dry-run] Would add 2 forwards to .kportal.yaml
  10000 → my-cluster/default/service/api:8080
  10001 → my-cluster/default/service/postgres:5432

Debugging

HTTP request log per forward

Set httpLog on a forward and press l to see its requests and responses. Open an entry with Enter to read headers, bodies and timing.

JSON bodies are pretty-printed, gzip and deflate bodies are decoded and binary bodies show a placeholder. f cycles the filter (All, Non-2xx, Errors), / searches path or method and c in the detail view copies the response body.

maxBodySize defaults to 1 MB when you omit it or set a value of 0 or less.

.kportal.yaml
forwards:
  - resource: service/api
    port: 8080
    localPort: 8080
    httpLog:
      enabled: true
      includeHeaders: true   # sensitive values are redacted
      maxBodySize: 65536     # bytes
      filterPath: "/api/"    # log only paths containing this text
      logFile: "api.log"     # also append entries to this file
  TIME        METHOD   STATUS  LATENCY   PATH
  ────────────────────────────────────────────────
  14:02:11    GET      200     12ms      /api/users
  14:02:12    POST     201     48ms      /api/users
  14:02:15    GET      404     9ms       /api/users/42

Security

Sensitive header values are redacted

With includeHeaders: true, kportal replaces the value of sensitive headers with [REDACTED] and keeps the header name. You cannot turn this off.

You see that an Authorization header was present without seeing its value.

Redacted by name:

  • Authorization
  • Proxy-Authorization
  • Cookie
  • Set-Cookie
  • X-Api-Key
  • X-Auth-Token
  • X-Csrf-Token
  • X-Access-Token

Redacted when the name contains (any case):

  • token
  • secret
  • password
  • apikey

Source: internal/httplog/proxy.go.

Debugging

Benchmark a forward

Press b on a forward. Set the URL path (default /), the HTTP method, the concurrency and the request count. The wizard starts at 10 workers and 100 requests.

The results screen reports totals, latency in milliseconds, requests per second, bytes read and the status code distribution. Only 2xx responses count as successful.

Total Requests:  100
Successful:      100 (100.0%)
Failed:          0

Latency (ms)
  Min:    x.xx
  Max:    x.xx
  Avg:    x.xx
  P50:    x.xx
  P95:    x.xx
  P99:    x.xx

Throughput
  Requests/sec:  x.xx
  Bytes read:    n

Status Codes
  200: n

Layout from internal/ui/wizard_views.go. The page shows no measured numbers.

Networking

mDNS hostnames

With mdns.enabled: true, each forward is reachable as <alias>.local. Without an alias, the resource name is used: service/redis becomes redis.local.

A bare pod with a selector and no alias is not published. Services register as _kportal._tcp.

.kportal.yaml
mdns:
  enabled: true

contexts:
  - name: production
    namespaces:
      - name: default
        forwards:
          - resource: service/postgres
            port: 5432
            localPort: 5432
            alias: prod-db   # reachable as prod-db.local:5432
check registration
dns-sd -B _kportal._tcp local       # macOS
avahi-browse -t _kportal._tcp       # Linux

Automation

Headless mode and config validation

-headless runs without the UI and writes logs to stderr. -v sets the log level, and -log-format json switches the format from text.

-check validates the config and exits. Ctrl+C and SIGTERM shut down cleanly.

Without -headless, -v prints a plain-text table that redraws.

shell
kportal -headless -v 2>kportal.log &
kportal -headless -log-format json
kportal -check
kportal -c /path/to/config.yaml

Tooling

Import from kftray, shell completion, updates

-convert turns a kftray JSON file into kportal YAML. -convert-output sets the output file (default .kportal.yaml).

kportal completion prints a bash, zsh or fish script, or installs it. Without --shell, kportal detects the shell.

-update checks for a newer release and -version prints the version.

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

kportal completion --shell zsh            # print the script
kportal completion --shell zsh --install  # install it
kportal completion --uninstall            # remove it

kportal -update
kportal -version

Supply chain

Installer that checks what it downloads

The install script downloads kportal-<version>-checksums.txt from the same release and verifies the archive SHA-256. It refuses to install on a mismatch.

If cosign is on PATH, it also verifies the keyless signature of the checksums file. Install options list the steps to verify a release by hand.

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

# download and verify only
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | DRY_RUN=1 bash

Comparison

kportal next to other port-forward tools

The table lists what each project documents. A cell reads "Not documented" when the project's README and docs do not say.

Compared on 2026-10-02 against each project's README and docs. Check their repositories for changes. The kportal column comes from this repository's code.

Feature comparison of kportal, kubectl port-forward, k9s, kubefwd and kftray
Feature kportal kubectl port-forward k9s kubefwd kftray
Many forwards in one config fileYesNoPartialannotations configure forwards; no forward list filePartialexample.fwdconf.yml existsYes
Automatic reconnectYesNot documentedNot documentedYesYes
Follows pod restarts by label or name prefixYesNot documentedNot documentedYeslabel filterYes
Terminal UI of all forwardsYesNoYesPortForward view, alias pf; forwards last for the sessionYesYes
GUINoNoNoNoYes
Config hot reloadYesfile watcher and SIGHUPNoPartialbenchmarks.yaml onlyNot documentedNot documented
HTTP request and response loggingYesNoNot documentedNot documentedYes
Health checksYestcp-dial or data-transferNot documentedNot documentedNot documentedPartialREADME says it tracks pod health
mDNS or friendly hostnamesYesmDNS, <alias>.localNoNoPartialedits /etc/hosts, not mDNS; needs rootNot documented
Built-in benchmarkYesNoYeshey, key bNot documentedNot documented
Imports another tool's configYeskftray JSONNoNoNot documentedPartialJSON, GitHub repos, service annotations; not other tools' formats
Several contexts in one configYesNoPartialPartialNot documented
Local port conflict detectionYesreports the holding processPartial:remotePort picks a free portNot documentedYesNot documented
PlatformsLinux, macOS, WindowsNot documentedmacOS, Linux, WindowsmacOS, Linux, WindowsmacOS, Linux, Windows
LicenceMITNot documentedApache-2.0Apache-2.0GPL-3.0
LanguageGoNot documentedGoGoRust (Tauri, Ratatui)
Latest release (2026-10-02)Releases pageNot documentedv0.51.0 (2026-06-06)v1.25.16 (2026-06-20)v0.29.2 (2026-09-29)

On narrow screens, scroll the table sideways. The first column stays in place.

When another tool fits better

  • kubectl port-forward for a one-off forward. It needs no config file and ships with kubectl.
  • k9s for general cluster browsing and its built-in benchmark.
  • kubefwd to forward every service in a namespace with local DNS names.
  • kftray for a tray GUI, shared JSON configs and UDP.

Kube Forwarder is a GUI-only forwarder. Its last release was 2019-08-14.

Sources

Install

Install kportal

Pick one method. Release archives cover Linux, macOS and Windows on amd64 and arm64.

Homebrew (macOS)

brew install --cask lukaszraczylo/taps/kportal

If you installed the formula (brew install lukaszraczylo/taps/kportal) earlier, run brew uninstall kportal first.

Install script

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

The script downloads kportal-<version>-checksums.txt from the same release and verifies the archive SHA-256. If cosign is on PATH, it also verifies the keyless signature of the checksums file.

VariableEffect
DRY_RUN=1Download and verify only. Do not install.
SKIP_COSIGN=1Skip the cosign check. The SHA-256 check still runs.
INSTALL_DIRTarget directory. Default /usr/local/bin.
KPORTAL_VERSIONInstall a specific version, for example 1.2.3, instead of the latest.
COSIGN_CERT_IDENTITY_REGEXPCertificate identity to accept. Set it if you fork the release pipeline.
example
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | DRY_RUN=1 bash

Manual download

The releases page has one archive per platform. Archives are named kportal-<version>-<os>-<arch>.tar.gz, and .zip on Windows. Each release also has a kportal-<version>-checksums.txt file.

Build from source

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

The build needs Go 1.26 or later (see go.mod). make install copies the binary to /opt/homebrew/bin if that directory exists, otherwise to ~/.local/bin on macOS and Linux.

Verify a release signature

Release checksums are signed with cosign using keyless signing. Download the checksums file and its sigstore bundle from the release, then run:

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

Replace <version> with the release version, for example 1.2.3. Then check the archive against the verified checksums file with shasum -a 256 or sha256sum.

Quick start

From install to first forward

Create .kportal.yaml in the directory where you run kportal, then start it.

.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
run
kportal

Each name under contexts must exist in your kubeconfig. List them with kubectl config get-contexts.

Keyboard shortcuts

Forwards list

KeyAction
↑ ↓ / j kMove the selection
PgUp PgDn / Ctrl+u Ctrl+dMove by 10 rows
Space / EnterToggle the forward
nAdd a forward
eEdit the forward
dDelete the forward (asks to confirm)
bBenchmark the forward
lOpen the HTTP log
q / Ctrl+cQuit

HTTP log, list view

KeyAction
↑ ↓Move
EnterOpen details
g / GTop / bottom
aToggle auto-scroll
fCycle filter: All, Non-2xx, Errors
/Search path or method
cClear filters
qClose

HTTP log, detail view

KeyAction
↑ ↓ / PgUp PgDnScroll
gTop
cCopy the response body
Esc / q / EnterBack to the list

Documentation

Reference

Command line, config keys and status values.

Command line

Command or flagEffect
kportalStart the terminal UI.
-c <file>Config file. Default .kportal.yaml.
-vVerbose logging. In headless mode it sets the log level only.
-headlessNo UI. Logs go to stderr.
-checkValidate the config and exit.
-context <name>Forward only the named contexts. Repeatable and comma-separated.
-log-format text|jsonLog format. Default text.
-convert <file>Convert a kftray JSON file to kportal YAML.
-convert-output <file>Output of -convert. Default .kportal.yaml.
-updateCheck for updates and exit.
-versionPrint the version and exit.
kportal generateAdd forwards from a cluster. Flags: --context (required), --config, --dry-run.
kportal completionPrint or install a shell completion script. Flags: --shell, --install, --uninstall.

Each flag also accepts the double-dash form, for example --check.

Forward options

KeyRequiredDescription
resourceYesservice/<name>, pod/<name> (exact name or prefix), or pod with selector
protocolNotcp. No other value is accepted.
portYesRemote port
localPortYesLocal port
aliasNoDisplay name and mDNS hostname
selectorWith bare podLabel selector. Not allowed with pod/<name>.
httpLogNotrue, or a map with enabled, includeHeaders, maxBodySize, filterPath, logFile

Top-level keys

KeyDefaultDescription
healthCheck.interval3sTime between checks
healthCheck.timeout2sTimeout of one check
healthCheck.methoddata-transfertcp-dial or data-transfer
healthCheck.maxConnectionAge25mReconnect an idle connection older than this
healthCheck.maxIdleTime10mA connection idle longer than this is stale
reliability.tcpKeepalive30sTCP keepalive interval
reliability.dialTimeout30sConnection dial timeout
reliability.retryOnStaletrueReconnect when a stale connection is detected
reliability.watchdogPeriod30sInterval of the check for hung workers
mdns.enabledoffPublish <alias>.local for each forward
tui.columnsall columnsList of name and optional width

Status indicators

IndicatorMeaning
● ActiveConnection healthy
○ StartingFirst connection. Failed health checks show Starting for the first 10 seconds.
◐ ReconnectingReconnecting after a failure
✗ ErrorConnection failed
○ DisabledTurned off in the UI

The -v table uses the same words. When stdout is not a terminal, it draws ✓ Active, ⋯ Starting, ↻ Reconnecting and ✗ Error.

Migrate from kftray

Convert a kftray JSON export to kportal YAML:

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

Signals

SignalEffect
Ctrl+C, SIGTERMGraceful shutdown
SIGHUPReload the configuration

Troubleshooting

Common failures

Port already in use

lsof -i :<port>
kill <pid>

Connection refused

  1. Check the pod: kubectl get pods -n <namespace>
  2. Check the port: kubectl describe pod <pod>
  3. Check the service endpoints: kubectl get endpoints <service>

Context not found

kubectl config get-contexts

kportal reports context <name> not found in kubeconfig when a contexts[].name value is not in your kubeconfig. Names with @, ., : or / (for example admin@home or an EKS ARN) are valid. If --context reports unknown context, the name is missing from contexts[].name in the config file.

Namespace prompt instead of a list

The cluster refused the namespace list or returned an empty one. Type the namespace name.

FAQ

Questions and answers

Does kportal support UDP?

No. Only tcp is accepted for protocol, and kportal generate skips non-TCP service ports.

Can kportal forward to a deployment?

No. Valid resource types are pod and service. To reach a deployment, use pod with a label selector.

Does a config edit need a restart?

No. kportal reloads when the file changes. An invalid file is rejected and the previous config stays active.

How do I run kportal on a server or in a script?

Use -headless. Add -log-format json for structured logs and -check in CI to validate the file.

Does the HTTP log expose my tokens?

No. The value of Authorization, Cookie and other sensitive headers is replaced with [REDACTED]. See Features for the full list.

Does kportal collect data?

On startup it sends one anonymous adoption ping with the project name, version and timestamp. The timeout is 2 seconds. Set DO_NOT_TRACK=1, OSS_TELEMETRY_DISABLED=1 or KPORTAL_DISABLE_TELEMETRY=1 to opt out. Details: oss-telemetry.