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
AuthorizedPrincipalsCommandconfiguration option of the OpenSSHsshd. -
ssh-cert-authorize
Specialized tool designed for direct use in the
AuthorizedPrincipalsCommandof 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-authorizetool
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
--debugflag - detailed parsing and validation steps
Log Message Types:
-
Certificate Processed (INFO level)
action=cert_processed serial=12345 extension=ssh-domain-grant@core.aai.geant.org ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8Logged immediately after successful certificate parsing. Shows the extension being processed and the CA that signed the certificate.
-
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/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8Logged 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. -
Authorization Denied (INFO level)
action=denied user=bob domain=login.another-example.com reason="domain not authorized" serial=67890 ca_fingerprint=SHA256:xK8xhhtXx8/GVCMvgWaeuAJXxd9by3pgZ1StnYla5k8Logged when authorization fails. The
reasonfield 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-fileflag 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-authorizeprocesses the extension successfully, as shown by theaction=cert_processedlog 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 (matchesweb.example.com, notapi.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
- Pure Go implementation: No external process execution required
- Static linking: Use
make static-buildfor minimal attack surface - No temp files: All processing done in memory
- Input validation: Strict validation of certificate and extension data
- Privilege separation: Runs as unprivileged user (typically
nobody) - 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.