diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8d7faff --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,52 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.24" + - uses: golangci/golangci-lint-action@v6 + with: + version: latest + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.24" + - run: go test ./... -race -coverprofile=coverage.out -covermode=atomic + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.out + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.24" + - run: CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags="-s -w" -o /dev/null ./cmd/probe diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ffecfa6 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version: "1.24" + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..09ec7cf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.24-alpine AS build +ARG VERSION=dev +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X main.version=${VERSION}" -o /probe ./cmd/probe + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /probe /probe +COPY probe.example.yaml /etc/krakenkey-probe/probe.yaml +VOLUME ["/var/lib/krakenkey-probe"] +EXPOSE 8080 +ENTRYPOINT ["/probe"] +CMD ["--config", "/etc/krakenkey-probe/probe.yaml"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5cd65a0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (c) 2026 KrakenKey + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..676dcea --- /dev/null +++ b/README.md @@ -0,0 +1,295 @@ +# KrakenKey Probe + +[![CI](https://github.com/krakenkey/probe/actions/workflows/ci.yaml/badge.svg)](https://github.com/krakenkey/probe/actions/workflows/ci.yaml) +[![Release](https://github.com/krakenkey/probe/releases/latest)](https://github.com/krakenkey/probe/releases/latest) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + +A lightweight TLS monitoring agent that scans endpoints for certificate health and reports results to the [KrakenKey](https://krakenkey.io) platform. Run it on your own infrastructure to monitor internal and external TLS certificates from a single dashboard. + +The probe connects to configured endpoints via TLS, extracts certificate and connection metadata (expiry, issuer, SANs, chain validity, TLS version, handshake latency), and sends results to the KrakenKey API on a configurable schedule. + +## Quick Start + +```bash +docker run -d \ + -e KK_PROBE_API_KEY="kk_your_api_key" \ + -e KK_PROBE_ENDPOINTS="example.com:443,api.example.com:443" \ + -e KK_PROBE_NAME="my-probe" \ + -v probe_state:/var/lib/krakenkey-probe \ + ghcr.io/krakenkey/probe:latest +``` + +## Configuration + +The probe is configured via a YAML file and/or environment variables. Environment variables take precedence over YAML values. + +### YAML Config + +```yaml +api: + url: "https://api.krakenkey.io" # KK_PROBE_API_URL + key: "kk_..." # KK_PROBE_API_KEY (required) + +probe: + id: "" # KK_PROBE_ID (auto-generated if empty) + name: "my-probe" # KK_PROBE_NAME + mode: "self-hosted" # KK_PROBE_MODE (self-hosted | hosted) + region: "" # KK_PROBE_REGION + interval: "60m" # KK_PROBE_INTERVAL (min: 1m, max: 24h) + timeout: "10s" # KK_PROBE_TIMEOUT + state_file: "/var/lib/krakenkey-probe/state.json" # KK_PROBE_STATE_FILE + +endpoints: # KK_PROBE_ENDPOINTS (comma-separated host:port) + - host: "example.com" + port: 443 + sni: "" # optional SNI override + - host: "internal.corp.net" + port: 8443 + +health: + enabled: true # KK_PROBE_HEALTH_ENABLED + port: 8080 # KK_PROBE_HEALTH_PORT + +logging: + level: "info" # KK_PROBE_LOG_LEVEL (debug|info|warn|error) + format: "json" # KK_PROBE_LOG_FORMAT (json|text) +``` + +### Environment Variable Reference + +| Variable | Default | Description | +|---|---|---| +| `KK_PROBE_API_URL` | `https://api.krakenkey.io` | KrakenKey API base URL | +| `KK_PROBE_API_KEY` | (required) | API key, must start with `kk_` | +| `KK_PROBE_ID` | (auto-generated) | Probe ID, persisted to state file | +| `KK_PROBE_NAME` | | Human-friendly probe name | +| `KK_PROBE_MODE` | `self-hosted` | `self-hosted` or `hosted` | +| `KK_PROBE_REGION` | | Geographic region label | +| `KK_PROBE_INTERVAL` | `60m` | Scan interval (Go duration) | +| `KK_PROBE_TIMEOUT` | `10s` | Per-endpoint TLS dial timeout | +| `KK_PROBE_STATE_FILE` | `/var/lib/krakenkey-probe/state.json` | State file path | +| `KK_PROBE_ENDPOINTS` | | Comma-separated `host:port` pairs | +| `KK_PROBE_HEALTH_ENABLED` | `true` | Enable health endpoint | +| `KK_PROBE_HEALTH_PORT` | `8080` | Health endpoint port | +| `KK_PROBE_LOG_LEVEL` | `info` | Log level | +| `KK_PROBE_LOG_FORMAT` | `json` | Log format | + +## Docker Compose + +```yaml +services: + krakenkey-probe: + image: ghcr.io/krakenkey/probe:latest + container_name: krakenkey-probe + restart: unless-stopped + environment: + KK_PROBE_API_KEY: "kk_your_api_key_here" + KK_PROBE_NAME: "my-probe" + KK_PROBE_ENDPOINTS: "example.com:443,api.example.com:443" + KK_PROBE_INTERVAL: "30m" + volumes: + - probe_state:/var/lib/krakenkey-probe + ports: + - "8080:8080" + +volumes: + probe_state: +``` + +## Kubernetes + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: krakenkey-probe +data: + probe.yaml: | + api: + url: "https://api.krakenkey.io" + probe: + name: "k8s-probe" + interval: "30m" + endpoints: + - host: "example.com" + port: 443 + - host: "api.example.com" + port: 443 + health: + enabled: true + port: 8080 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: krakenkey-probe +spec: + replicas: 1 + selector: + matchLabels: + app: krakenkey-probe + template: + metadata: + labels: + app: krakenkey-probe + spec: + containers: + - name: probe + image: ghcr.io/krakenkey/probe:latest + args: ["--config", "/etc/krakenkey-probe/probe.yaml"] + env: + - name: KK_PROBE_API_KEY + valueFrom: + secretKeyRef: + name: krakenkey-probe-secret + key: api-key + ports: + - containerPort: 8080 + name: health + livenessProbe: + httpGet: + path: /healthz + port: health + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /readyz + port: health + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: config + mountPath: /etc/krakenkey-probe + readOnly: true + - name: state + mountPath: /var/lib/krakenkey-probe + volumes: + - name: config + configMap: + name: krakenkey-probe + - name: state + emptyDir: {} +``` + +## Building from Source + +```bash +# Clone +git clone https://github.com/krakenkey/probe.git +cd probe + +# Build +go build -ldflags="-s -w -X main.version=0.1.0" -o krakenkey-probe ./cmd/probe + +# Run +./krakenkey-probe --config probe.example.yaml +``` + +### Cross-compile + +```bash +# Linux ARM64 +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o krakenkey-probe ./cmd/probe + +# macOS ARM64 (Apple Silicon) +CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o krakenkey-probe ./cmd/probe +``` + +## How It Works + +``` + +-----------+ + | KrakenKey | + | API | + +-----^-----+ + | + POST /probes/report + | ++----------------+ +-----+-----+ +------------------+ +| probe.yaml / | ----> | KrakenKey | ----> | TLS Endpoints | +| env vars | | Probe | | (host:port) | ++----------------+ +-----------+ +------------------+ + | + GET /healthz + GET /readyz +``` + +1. On startup, the probe loads its config, generates or reads its probe ID, and registers with the KrakenKey API. +2. It immediately runs the first scan cycle: connects to each endpoint via TLS, extracts certificate metadata, and sends results to the API. +3. After the first scan, the `/readyz` endpoint returns `200 OK`. +4. Subsequent scans run on the configured interval. +5. On `SIGINT` or `SIGTERM`, the probe finishes the current scan cycle and shuts down. + +### What Gets Collected + +For each endpoint, the probe extracts: + +**Connection metadata:** +- TLS protocol version (1.0, 1.1, 1.2, 1.3) +- Negotiated cipher suite +- TLS handshake latency (ms) +- OCSP stapling status + +**Certificate metadata:** +- Subject CN and SANs +- Issuer chain +- Serial number +- Validity period and days until expiry +- Key type and size (RSA/ECDSA/Ed25519) +- Signature algorithm +- SHA-256 fingerprint +- Chain depth and completeness +- Trust status (verified against system cert pool) + +## API Key Setup + +1. Log in to [KrakenKey](https://app.krakenkey.io) +2. Navigate to **API Keys** in the dashboard +3. Create a new API key +4. Set `KK_PROBE_API_KEY` to the generated key (starts with `kk_`) + +## Health Endpoints + +| Endpoint | Description | +|---|---| +| `GET /healthz` | Always returns `200 OK` with probe status, version, mode, and scan times | +| `GET /readyz` | Returns `503` until the first scan completes, then `200 OK` | + +### `/healthz` Response + +```json +{ + "status": "ok", + "version": "0.1.0", + "probeId": "a1b2c3d4-...", + "mode": "self-hosted", + "region": "us-east-1", + "lastScan": "2026-03-17T12:00:00Z", + "nextScan": "2026-03-17T13:00:00Z" +} +``` + +## Troubleshooting + +**"api.key is required"** +Set `KK_PROBE_API_KEY` or add `api.key` to your config file. + +**"API authentication failed (HTTP 401): check your API key"** +The API key is invalid or expired. Generate a new one from the KrakenKey dashboard. + +**"dial tcp: ... connection refused"** +The endpoint is not reachable from the probe's network. Check firewall rules, DNS resolution, and that the service is running on the expected port. + +**"dial tcp: ... i/o timeout"** +The endpoint is not responding within the configured timeout. Increase `KK_PROBE_TIMEOUT` or check network connectivity. + +**"API rate limited (HTTP 429)"** +The probe is sending reports too frequently. Increase `KK_PROBE_INTERVAL`. + +**Probe ID keeps changing** +Ensure the state file path is persistent across restarts. When using Docker, mount a volume to `/var/lib/krakenkey-probe`. + +## License + +[AGPL-3.0](LICENSE) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..8f65604 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,17 @@ +services: + krakenkey-probe: + image: ghcr.io/krakenkey/probe:latest + container_name: krakenkey-probe + restart: unless-stopped + environment: + KK_PROBE_API_KEY: "kk_your_api_key_here" + KK_PROBE_NAME: "my-probe" + KK_PROBE_ENDPOINTS: "example.com:443,api.example.com:443,internal.corp:8443" + KK_PROBE_INTERVAL: "30m" + volumes: + - probe_state:/var/lib/krakenkey-probe + ports: + - "8080:8080" + +volumes: + probe_state: diff --git a/goreleaser.yaml b/goreleaser.yaml new file mode 100644 index 0000000..0e2a822 --- /dev/null +++ b/goreleaser.yaml @@ -0,0 +1,80 @@ +version: 2 + +project_name: krakenkey-probe + +before: + hooks: + - go mod tidy + +builds: + - id: probe + main: ./cmd/probe + binary: krakenkey-probe + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X main.version={{ .Version }} + +archives: + - id: probe + builds: + - probe + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + format: tar.gz + +dockers: + - image_templates: + - "ghcr.io/krakenkey/probe:{{ .Version }}-amd64" + - "ghcr.io/krakenkey/probe:latest-amd64" + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=VERSION={{ .Version }}" + dockerfile: Dockerfile + goos: linux + goarch: amd64 + - image_templates: + - "ghcr.io/krakenkey/probe:{{ .Version }}-arm64" + - "ghcr.io/krakenkey/probe:latest-arm64" + use: buildx + build_flag_templates: + - "--platform=linux/arm64" + - "--build-arg=VERSION={{ .Version }}" + dockerfile: Dockerfile + goos: linux + goarch: arm64 + +docker_manifests: + - name_template: "ghcr.io/krakenkey/probe:{{ .Version }}" + image_templates: + - "ghcr.io/krakenkey/probe:{{ .Version }}-amd64" + - "ghcr.io/krakenkey/probe:{{ .Version }}-arm64" + - name_template: "ghcr.io/krakenkey/probe:{{ .Major }}.{{ .Minor }}" + image_templates: + - "ghcr.io/krakenkey/probe:{{ .Version }}-amd64" + - "ghcr.io/krakenkey/probe:{{ .Version }}-arm64" + - name_template: "ghcr.io/krakenkey/probe:{{ .Major }}" + image_templates: + - "ghcr.io/krakenkey/probe:{{ .Version }}-amd64" + - "ghcr.io/krakenkey/probe:{{ .Version }}-arm64" + - name_template: "ghcr.io/krakenkey/probe:latest" + image_templates: + - "ghcr.io/krakenkey/probe:latest-amd64" + - "ghcr.io/krakenkey/probe:latest-arm64" + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^chore:" + - "^ci:"