Files
lukaszraczylo 0a8c872b01 fix(install): pin cosign cert-identity to shared-actions workflow
Releases are signed by the lukaszraczylo/shared-actions reusable
workflow, so the Sigstore certificate subject is the workflow URL
rather than this repo. The previous regex
'https://github.com/lukaszraczylo/kportal/.*' never matched, so any
user with cosign installed would see verification fail and abort
the install.

Pin cert-identity to the exact workflow URL:
  ^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$

Override via COSIGN_CERT_IDENTITY_REGEXP for forks of the release
pipeline. Same fix applied to README's manual verification example.

Verified end-to-end against release v0.2.90:
  cosign verify-blob ... -> Verified OK
2026-05-06 11:02:40 +01:00

301 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
set -e
# kportal installation script
# Usage: curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
#
# Environment overrides:
# INSTALL_DIR - target install directory (default: /usr/local/bin)
# KPORTAL_VERSION - install a specific version instead of latest (e.g. 1.2.3)
# DRY_RUN=1 - download and verify but do not install (for local testing)
# SKIP_COSIGN=1 - skip cosign signature verification even if cosign is present
REPO="lukaszraczylo/kportal"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
DRY_RUN="${DRY_RUN:-0}"
SKIP_COSIGN="${SKIP_COSIGN:-0}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print functions
print_info() {
echo -e "${BLUE}i${NC} $1"
}
print_success() {
echo -e "${GREEN}OK${NC} $1"
}
print_error() {
echo -e "${RED}X${NC} $1" >&2
}
print_warning() {
echo -e "${YELLOW}!${NC} $1"
}
# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) echo "linux";;
Darwin*) echo "darwin";;
MINGW*|MSYS*|CYGWIN*) echo "windows";;
*) echo "unknown";;
esac
}
# Detect architecture
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "amd64";;
aarch64|arm64) echo "arm64";;
armv7l) echo "arm";;
*) echo "unknown";;
esac
}
# Get latest version from GitHub
get_latest_version() {
curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" |
grep '"tag_name":' |
sed -E 's/.*"v([^"]+)".*/\1/'
}
# Compute sha256 of a file. Uses shasum which is available on macOS and Linux.
compute_sha256() {
local file="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "${file}" | awk '{ print $1 }'
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | awk '{ print $1 }'
else
print_error "Neither 'shasum' nor 'sha256sum' is available; cannot verify checksum"
exit 1
fi
}
# Verify the archive against checksums.txt (SHA-256). Aborts on mismatch.
verify_checksum() {
local archive="$1"
local checksums_file="$2"
print_info "Verifying SHA-256 checksum..."
local expected
# Match the archive name as the second whitespace-separated field.
# checksums.txt format produced by goreleaser: "<sha256> <filename>"
expected=$(awk -v name="${archive}" '$2 == name { print $1; exit }' "${checksums_file}")
if [ -z "${expected}" ]; then
print_error "Checksum for ${archive} not found in checksums.txt"
print_error "Refusing to install unverified binary."
exit 1
fi
local actual
actual=$(compute_sha256 "${archive}")
if [ "${expected}" != "${actual}" ]; then
print_error "Checksum mismatch for ${archive}"
print_error " expected: ${expected}"
print_error " actual: ${actual}"
print_error "Aborting installation. The downloaded archive may be corrupted or tampered with."
exit 1
fi
print_success "SHA-256 checksum OK"
}
# Optional: verify cosign signature on the checksums file. Silently skipped
# when cosign is not installed or the signature artefact is not present.
verify_cosign_signature() {
local checksums_file="$1"
local sig_file="$2"
if [ "${SKIP_COSIGN}" = "1" ]; then
return 0
fi
if ! command -v cosign >/dev/null 2>&1; then
# cosign not installed; supply-chain integrity still rests on SHA-256
return 0
fi
if [ ! -f "${sig_file}" ]; then
# No sig artefact downloaded; skip silently
return 0
fi
print_info "Verifying cosign signature on checksums.txt..."
# Releases are signed by the shared-actions reusable workflow, so the
# cert subject is the workflow URL — NOT this repo. Override with
# COSIGN_CERT_IDENTITY_REGEXP if you fork the release pipeline.
local cert_identity_regexp="${COSIGN_CERT_IDENTITY_REGEXP:-^https://github\.com/lukaszraczylo/shared-actions/\.github/workflows/go-release\.yaml@refs/heads/main$}"
if cosign verify-blob \
--certificate-identity-regexp "${cert_identity_regexp}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "${sig_file}" \
"${checksums_file}" >/dev/null 2>&1; then
print_success "cosign signature OK"
else
print_error "cosign signature verification FAILED for checksums.txt"
print_error "Aborting installation."
exit 1
fi
}
# Main installation
main() {
echo ""
echo "kportal installation script"
echo "Kubernetes port forwarding made easy"
echo ""
# Detect system
OS=$(detect_os)
ARCH=$(detect_arch)
if [ "$OS" = "unknown" ] || [ "$ARCH" = "unknown" ]; then
print_error "Unsupported operating system or architecture"
print_info "OS: $(uname -s), Arch: $(uname -m)"
exit 1
fi
print_info "Detected: ${OS}/${ARCH}"
# Get version
if [ -n "${KPORTAL_VERSION:-}" ]; then
VERSION="${KPORTAL_VERSION#v}"
print_info "Using requested version: v${VERSION}"
else
print_info "Fetching latest version..."
VERSION=$(get_latest_version)
if [ -z "$VERSION" ]; then
print_error "Failed to fetch latest version"
exit 1
fi
print_success "Latest version: v${VERSION}"
fi
# Construct download URLs
if [ "$OS" = "windows" ]; then
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.zip"
else
ARCHIVE="kportal-${VERSION}-${OS}-${ARCH}.tar.gz"
fi
BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}"
DOWNLOAD_URL="${BASE_URL}/${ARCHIVE}"
CHECKSUMS_FILE="kportal-${VERSION}-checksums.txt"
CHECKSUMS_URL="${BASE_URL}/${CHECKSUMS_FILE}"
SIG_FILE="${CHECKSUMS_FILE}.sigstore.json"
SIG_URL="${BASE_URL}/${SIG_FILE}"
# Create temporary directory
TMP_DIR=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '${TMP_DIR}'" EXIT
# Download archive
print_info "Downloading ${ARCHIVE}..."
if ! curl -fsSL -o "${TMP_DIR}/${ARCHIVE}" "${DOWNLOAD_URL}"; then
print_error "Failed to download kportal archive"
print_info "URL: ${DOWNLOAD_URL}"
exit 1
fi
# Download checksums
print_info "Downloading checksums.txt..."
if ! curl -fsSL -o "${TMP_DIR}/${CHECKSUMS_FILE}" "${CHECKSUMS_URL}"; then
print_error "Failed to download checksums file"
print_info "URL: ${CHECKSUMS_URL}"
print_error "Refusing to install without checksum verification."
exit 1
fi
# Try to download cosign signature bundle (best-effort, non-fatal if absent)
if curl -fsSL -o "${TMP_DIR}/${SIG_FILE}" "${SIG_URL}" 2>/dev/null; then
:
else
rm -f "${TMP_DIR}/${SIG_FILE}"
fi
# Verify archive checksum
cd "${TMP_DIR}"
verify_checksum "${ARCHIVE}" "${CHECKSUMS_FILE}"
# Optional cosign signature verification on checksums file
verify_cosign_signature "${CHECKSUMS_FILE}" "${SIG_FILE}"
# Extract archive
print_info "Extracting archive..."
if [ "$OS" = "windows" ]; then
unzip -q "${ARCHIVE}"
BINARY="kportal.exe"
else
tar -xzf "${ARCHIVE}"
BINARY="kportal"
fi
# Check if binary exists
if [ ! -f "${BINARY}" ]; then
print_error "Binary not found after extraction"
exit 1
fi
# Make binary executable
chmod +x "${BINARY}"
if [ "${DRY_RUN}" = "1" ]; then
print_success "Dry run successful. Verified archive at ${TMP_DIR}/${ARCHIVE}"
print_info "Skipping install step (DRY_RUN=1)"
return 0
fi
# Install binary
print_info "Installing kportal to ${INSTALL_DIR}..."
# Check if we need sudo
if [ ! -w "${INSTALL_DIR}" ]; then
print_warning "Installation directory requires sudo access"
if command -v sudo >/dev/null 2>&1; then
sudo mv "${BINARY}" "${INSTALL_DIR}/${BINARY}"
else
print_error "sudo not found. Please run with appropriate permissions"
exit 1
fi
else
mv "${BINARY}" "${INSTALL_DIR}/${BINARY}"
fi
# Verify installation (portable: awk instead of GNU-only grep -oP)
if command -v kportal >/dev/null 2>&1; then
INSTALLED_VERSION=$(kportal --version 2>/dev/null | awk '/^kportal version/ { print $3; exit }')
if [ -z "${INSTALLED_VERSION}" ]; then
INSTALLED_VERSION="unknown"
fi
print_success "kportal v${INSTALLED_VERSION} installed successfully!"
else
print_warning "kportal installed but not found in PATH"
print_info "You may need to add ${INSTALL_DIR} to your PATH"
fi
echo ""
print_success "Installation complete!"
echo ""
echo "Get started:"
echo " 1. Create a config file: touch .kportal.yaml"
echo " 2. Run: kportal"
echo ""
echo "Documentation: https://lukaszraczylo.github.io/kportal"
echo ""
}
main "$@"