ssh-cert-tool

module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2025 License: Apache-2.0

README

SSH Certificate Tool

A Go implementation for parsing and validating SSH certificates with domain authorization extensions. This project provides two command-line tools for different use cases.

Binaries

  • ssh-cert-tool

    General-purpose certificate parsing tool for analyzing SSH certificates and extracting domain authorization extensions. Provides both human-readable and JSON output formats. It is meant to be integrated in existing scripts / tools that are used with the AuthorizedPrincipalsCommand configuration option of the OpenSSH sshd.

  • ssh-cert-authorize

    Specialized tool designed for direct use in the AuthorizedPrincipalsCommand of OpenSSH.

Features

  • Two specialized binaries: Optimized for different use cases
  • Pure Go implementation: No external dependencies or ssh-keygen required
  • Domain authorization: Support for domain grant extensions
  • High performance: Fast parsing and low memory usage
  • Cross-platform: Builds for Linux, macOS, and Windows
  • JSON support: Structured output for integration with other tools
  • Structured logging: Machine-parseable logs for the ssh-cert-authorize tool

Quick Start

# Install the binaries
make build
sudo make install
ssh-cert-authorize
# Configure allowed domain
echo "login.example.com" > /etc/ssh/cert-allowed-domain.conf

# Add to /etc/ssh/sshd_config
echo "AuthorizedPrincipalsCommand /usr/local/bin/ssh-cert-authorize %u %k" >> /etc/ssh/sshd_config
echo "AuthorizedPrincipalsCommandUser nobody" >> /etc/ssh/sshd_config

# Restart SSH
sudo systemctl restart sshd

[!NOTE] Even though the examples above use sudo to install the tool, there is no requirement for root priviledges to use this. Deployers can choose to install the tool as non-priviledged user.

ssh-cert-tool
# Parse a certificate
cat cert.pub | ssh-cert-tool parse

# Parse with JSON output
cat cert.pub | ssh-cert-tool parse --json

Installation

Pre-built Binary

Download the appropriate binary for your platform from the releases page.

Build from Source
# Clone the repository
git clone https://geant.gitlab.org/core-aai-platform/ssh-cert-tool
cd ssh-cert-tool

# Build for current platform
make build
Cross-platform Build
# Build for all platforms
make build-all

# Creates binaries in build/:
# - ssh-cert-tool-linux-amd64
# - ssh-cert-tool-linux-arm64
# - ssh-cert-tool-darwin-amd64
# - ssh-cert-tool-darwin-arm64
# - ssh-cert-tool-windows-amd64.exe
# - ssh-cert-authorize-linux-amd64
# - ssh-cert-authorize-linux-arm64
# - ssh-cert-authorize-darwin-amd64
# - ssh-cert-authorize-darwin-arm64
# - ssh-cert-authorize-windows-amd64.exe
Static Build (for minimal dependencies)
# Build static binary (no libc dependency)
make static-build

Logging

The ssh-cert-authorize binary produces structured logs in a machine-parseable key=value format. This enables easy filtering, monitoring, and analysis of authentication events.

Log Output Configuration:

By default, ssh-cert-authorize logs to syslog. To log to stderr instead:

# Log to stderr (useful for testing/debugging)
AuthorizedPrincipalsCommand /usr/local/bin/ssh-cert-authorize --syslog=false %u %k

Log Levels:

  • ERROR: Always logged - failures and error conditions
  • INFO: Always logged - certificate processing and authorization decisions
  • DEBUG: Only with --debug flag - detailed parsing and validation steps

Log Message Types:

  1. Certificate Processed (INFO level)

    action=cert_processed serial=12345 extension=ssh-domain-grant@core.aai.geant.org ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8
    

    Logged immediately after successful certificate parsing. Shows the extension being processed and the CA that signed the certificate.

  2. Authorization Success (INFO level)

    action=authorized user=alice cert_principals=["admin","operator","viewer"] matched_principals=["admin","operator"] domain=login.example.com serial=12345 ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8
    

    Logged when a user is successfully authorized. Shows all principals from the certificate (cert_principals) and which ones matched the authorization policy (matched_principals). The matched principals are returned to sshd.

  3. Authorization Denied (INFO level)

    action=denied user=bob domain=login.another-example.com reason="domain not authorized" serial=67890 ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8
    

    Logged when authorization fails. The reason field explains why.

Common Denial Reasons:

  • "domain not authorized" - Certificate's authorized domains don't match the required domain
  • "no principals in certificate" - Certificate has no principals
  • "principals file required but not found" - The --require-principals-file flag is set but no principals file exists for this user.
  • "no matching principals" - Certificate principals don't match the principals file

Log Fields Reference:

Field Type Description Present In
action string Type of event (cert_processed, authorized, denied) All messages
serial int Certificate serial number All messages
ca_fingerprint string SHA256 fingerprint of signing CA (SSH format) All messages
extension string Extension ID being processed cert_processed
user string Username attempting to authenticate authorized, denied
cert_principals array JSON array of all principals from certificate authorized
matched_principals array JSON array of principals that matched authorization authorized
domain string Domain being authorized for authorized, denied
reason string Reason for denial (quoted if contains spaces) denied

Full Example with System Context:

Oct 23 15:34:12 login sshd[6398]: Certificate extension "ssh-domain-grant@core.aai.geant.org" is not supported
Oct 23 15:34:12 login sshd[6398]: Postponed publickey for alice from 192.168.5.2 port 36210 ssh2 [preauth]
Oct 23 15:34:12 login ssh-cert-authorize[6405]: action=cert_processed serial=12345 extension=ssh-domain-grant@core.aai.geant.org ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8
Oct 23 15:34:12 login ssh-cert-authorize[6405]: action=authorized user=alice cert_principals=["admin","operator"] matched_principals=["admin"] domain=web.example.com serial=12345 ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8
Oct 23 15:34:12 login sshd[6398]: Accepted publickey for alice from 192.168.5.2 port 36210 ssh2: ED25519-CERT SHA256:...

[!Note] The sshd message "Certificate extension ... is not supported" is normal and expected. OpenSSH logs this for all unknown extensions, but ssh-cert-authorize processes the extension successfully, as shown by the action=cert_processed log message.

Principals File Support

The ssh-cert-authorize binary can optionally use principals files to override certificate principals:

# Create principals directory
mkdir -p /etc/ssh/auth_principals

# Create principals file for user 'alice'
echo -e "admin\noperator" > /etc/ssh/auth_principals/alice

# When alice authenticates with a valid certificate:
# - If /etc/ssh/auth_principals/alice exists, those principals are used
# - If not, principals from the certificate are used
Using ssh-cert-tool Command Line Interface
Parse Certificate: STDIN
cat cert.pub | ssh-cert-tool parse

# Parse from argument
ssh-cert-tool parse "$(cat cert.pub)"

# Output format (standard):
PRINCIPALS:alice,bob
DOMAINS:*.example.com,staging.com
Parse Certificate: ARGUMENT
cat cert.pub | ssh-cert-tool parse

# Parse from argument
ssh-cert-tool parse "$(cat cert.pub)"

# Output format (standard):
PRINCIPALS:alice,bob
DOMAINS:*.example.com,staging.com
Parse with JSON output
cat cert.pub | ssh-cert-tool parse --json

Output format (JSON):

{
  "type": "ssh-ed25519-cert-v01@openssh.com user certificate",
  "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...",
  "signing_ca": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...",
  "signing_ca_fingerprint": "SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8",
  "key_id": "alice@example.com",
  "serial": 42,
  "valid_after": "2024-01-01T00:00:00",
  "valid_before": "2025-01-01T00:00:00",
  "principals": ["alice", "bob"],
  "extensions": {
    "domains": ["*.example.com", "staging.com"],
    "permit-pty": true,
    "permit-user-rc": true
  }
}

Command Reference

ssh-cert-tool Commands
Command Description Options
parse Parse certificate (default) --json, --extension
help Show help message None
version Show version information None
ssh-cert-authorize Options
Option Description Default
--domain Allowed domain (overrides file/env) (none)
--domain-file File with allowed domain /etc/ssh/cert-allowed-domain.conf
--domain-env Environment variable name (none)
--extension Extension identifier ssh-domain-grant@core.aai.geant.org
--principals-dir Directory with principals files /etc/ssh/auth_principals
--require-principals-file Require principals file false
--debug Enable debug logging false
--syslog Log to syslog true
--version Show version None
--help Show help None

Development

Running Tests
# Run all tests
make test

# Run all tests including cmd tests
make test-all

# Run with coverage
make test-coverage
Code Quality
# Format code
make fmt

# Run linter
make lint

# Run vet
make vet

Extension Format

The tool works with SSH certificate extensions using the following format:

  • Extension ID: ssh-domain-grant@core.aai.geant.org
  • Format: JSON array of domain strings
  • Encoding: Binary (raw bytes) in certificates, JSON in parsed output
Extension Data Structure

The extension value contains a JSON array of authorized domain patterns:

["*.example.com", "staging.com", "web.prod.com"]

Domain patterns support wildcards:

  • example.com - Exact match
  • *.example.com - Single-level wildcard (matches web.example.com, not api.web.example.com)
  • prefix-*.suffix.com - Wildcard in middle position

Using ssh-cert-tool in Scripts

#!/bin/bash
# cert-info.sh - Extract certificate information

CERT_FILE="$1"

# Parse certificate to JSON
CERT_JSON=$(cat "$CERT_FILE" | ssh-cert-tool parse --json)

# Extract fields using jq
KEY_ID=$(echo "$CERT_JSON" | jq -r '.key_id')
SERIAL=$(echo "$CERT_JSON" | jq -r '.serial')
CA_FINGERPRINT=$(echo "$CERT_JSON" | jq -r '.signing_ca_fingerprint')
PRINCIPALS=$(echo "$CERT_JSON" | jq -r '.principals | join(",")')
DOMAINS=$(echo "$CERT_JSON" | jq -r '.extensions.domains | join(",")')

echo "Certificate Information:"
echo "  Key ID: $KEY_ID"
echo "  Serial: $SERIAL"
echo "  CA Fingerprint: $CA_FINGERPRINT"
echo "  Certificate Principals: $PRINCIPALS"
echo "  Authorized Domains: $DOMAINS"
Monitoring with Structured Logs
#!/bin/bash
# Monitor authorization events from logs

# Find all authorization events for a specific user
grep 'action=authorized user=alice' /var/log/auth.log

# Find all denials
grep 'action=denied' /var/log/auth.log

# Track certificates by serial number
grep 'serial=12345' /var/log/auth.log

# Find all certificates signed by a specific CA
grep 'ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8' /var/log/auth.log

# Count authorization failures by reason
grep 'action=denied' /var/log/auth.log | \
  sed -n 's/.*reason=\([^ ]*\).*/\1/p' | \
  sort | uniq -c

Performance

The tool is optimized for high-performance operation:

  • Startup time: ~5ms
  • Memory usage: ~3MB
  • Certificate parsing: ~1ms per certificate
  • Domain matching: <1μs per match
  • No external dependencies: Pure Go implementation

Security Considerations

  1. Pure Go implementation: No external process execution required
  2. Static linking: Use make static-build for minimal attack surface
  3. No temp files: All processing done in memory
  4. Input validation: Strict validation of certificate and extension data
  5. Privilege separation: Runs as unprivileged user (typically nobody)
  6. Audit logging: Structured logs with CA fingerprints and serial numbers

Contributing

Contributions are welcome! Please submit pull requests or open issues for bugs and feature requests.

Directories

Path Synopsis
cmd
ssh-cert-tool command
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL