Files
certctl/internal/connector/target/iis/iis.go
T
2026-03-14 08:22:17 -04:00

197 lines
7.4 KiB
Go

package iis
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"runtime"
"time"
"github.com/shankar0123/certctl/internal/connector/target"
)
// Config represents the IIS deployment target configuration.
// This configuration is for Windows agents that manage IIS servers.
type Config struct {
Hostname string `json:"hostname"` // Target hostname or IP
SiteName string `json:"site_name"` // IIS site name (e.g., "Default Web Site")
CertStore string `json:"cert_store"` // Windows cert store (e.g., "My", "WebHosting")
BindingInfo string `json:"binding_info"` // Binding info (e.g., "*.example.com")
}
// Connector implements the target.Connector interface for IIS (Internet Information Services).
// This connector runs on Windows agents and manages certificate deployment via IIS.
//
// IIS certificate management requires:
// - Windows Server with IIS installed
// - PowerShell execution available
// - Administrative privileges
//
// TODO: Implement actual PowerShell command execution for:
// - Certificate import: Import-PfxCertificate
// - IIS binding update: New-WebBinding, Set-WebBinding
// - Validation: Get-WebBinding
type Connector struct {
config *Config
logger *slog.Logger
}
// New creates a new IIS target connector with the given configuration and logger.
func New(config *Config, logger *slog.Logger) *Connector {
return &Connector{
config: config,
logger: logger,
}
}
// ValidateConfig checks that the IIS configuration is valid and accessible.
// It verifies that we're on Windows and that the IIS site exists.
//
// TODO: Implement actual PowerShell checks.
func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessage) error {
var cfg Config
if err := json.Unmarshal(rawConfig, &cfg); err != nil {
return fmt.Errorf("invalid IIS config: %w", err)
}
if cfg.SiteName == "" || cfg.CertStore == "" {
return fmt.Errorf("IIS site_name and cert_store are required")
}
// Verify we're on Windows
if runtime.GOOS != "windows" {
return fmt.Errorf("IIS connector only runs on Windows, got %s", runtime.GOOS)
}
c.logger.Info("validating IIS configuration",
"site_name", cfg.SiteName,
"cert_store", cfg.CertStore,
"hostname", cfg.Hostname)
// TODO: Implement PowerShell check
// In production:
// 1. Run PowerShell command: Get-IISSite -Name {SiteName}
// 2. Verify site exists and is running
// 3. Check cert store: Get-Item -Path "Cert:\LocalMachine\{CertStore}"
c.logger.Warn("IIS validation not yet fully implemented",
"site_name", cfg.SiteName)
c.config = &cfg
return nil
}
// DeployCertificate imports a certificate to the Windows certificate store and updates
// the IIS binding to use the new certificate.
//
// The IIS deployment process (via PowerShell):
// 1. Create a temporary PFX file from the certificate and existing private key
// (Note: The private key is managed by the agent, not provided by the control plane)
// 2. Import the PFX to the Windows certificate store (My store by default)
// 3. Get the certificate thumbprint
// 4. Update the IIS binding to use the new certificate by thumbprint
// 5. Verify the binding is active
//
// TODO: Implement actual PowerShell commands:
// - Import-PfxCertificate -FilePath {pfxPath} -CertStoreLocation "Cert:\LocalMachine\My"
// - Get-ChildItem -Path "Cert:\LocalMachine\My" | Where {$_.Subject -eq "CN=..."}
// - Set-WebBinding -Name {SiteName} -BindingInformation "{BindingInfo}" -Protocol https -SslFlags 1 -CertificateThumbprint {thumbprint}
func (c *Connector) DeployCertificate(ctx context.Context, request target.DeploymentRequest) (*target.DeploymentResult, error) {
c.logger.Info("deploying certificate to IIS",
"site_name", c.config.SiteName,
"cert_store", c.config.CertStore)
startTime := time.Now()
// TODO: Implement IIS certificate deployment
// In production:
// 1. Create temporary PFX from CertPEM and ChainPEM
// (Private key should already exist on the agent)
// 2. Import certificate:
// PowerShell: Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\LocalMachine\{CertStore}" -Password $password
// 3. Get certificate thumbprint:
// PowerShell: (Get-ChildItem -Path "Cert:\LocalMachine\{CertStore}" | Where {$_.Subject -like "*CN=*"}).Thumbprint
// 4. Update IIS binding:
// PowerShell: Set-WebBinding -Name "{SiteName}" -BindingInformation "{BindingInfo}:443:*.example.com" -Protocol https -CertificateThumbprint $thumbprint
// 5. Remove temporary PFX file
deploymentDuration := time.Since(startTime)
c.logger.Warn("IIS deployment not yet implemented",
"site_name", c.config.SiteName)
return &target.DeploymentResult{
Success: true,
TargetAddress: fmt.Sprintf("%s (IIS: %s)", c.config.Hostname, c.config.SiteName),
DeploymentID: fmt.Sprintf("iis-%d", time.Now().Unix()),
Message: "Certificate deployment to IIS initiated (stub)",
DeployedAt: time.Now(),
Metadata: map[string]string{
"hostname": c.config.Hostname,
"site_name": c.config.SiteName,
"cert_store": c.config.CertStore,
"duration_ms": fmt.Sprintf("%d", deploymentDuration.Milliseconds()),
},
}, nil
}
// ValidateDeployment verifies that the certificate is properly deployed in IIS.
// It checks the IIS binding configuration to ensure it's active with the correct certificate.
//
// TODO: Implement actual PowerShell validation.
// PowerShell command:
// - Get-IISSiteBinding -Name {SiteName} | Where {$_.protocol -eq "https"}
func (c *Connector) ValidateDeployment(ctx context.Context, request target.ValidationRequest) (*target.ValidationResult, error) {
c.logger.Info("validating IIS deployment",
"certificate_id", request.CertificateID,
"serial", request.Serial,
"site_name", c.config.SiteName)
startTime := time.Now()
// TODO: Implement IIS deployment validation
// In production:
// 1. Query IIS binding status:
// PowerShell: Get-WebBinding -Name "{SiteName}" -Protocol "https"
// 2. Verify binding exists and is active
// 3. Extract certificate thumbprint from binding
// 4. Query certificate store to verify thumbprint matches expected certificate
// 5. Check certificate validity dates and key match
validationDuration := time.Since(startTime)
c.logger.Warn("IIS validation not yet implemented",
"site_name", c.config.SiteName)
return &target.ValidationResult{
Valid: true,
Serial: request.Serial,
TargetAddress: fmt.Sprintf("%s (IIS: %s)", c.config.Hostname, c.config.SiteName),
Message: "Certificate deployment validation initiated (stub)",
ValidatedAt: time.Now(),
Metadata: map[string]string{
"hostname": c.config.Hostname,
"site_name": c.config.SiteName,
"duration_ms": fmt.Sprintf("%d", validationDuration.Milliseconds()),
},
}, nil
}
// executePowerShellCommand is a helper to run PowerShell commands on Windows.
// It's a stub implementation that documents the pattern for actual PS execution.
func (c *Connector) executePowerShellCommand(ctx context.Context, psCommand string) (string, error) {
if runtime.GOOS != "windows" {
return "", fmt.Errorf("PowerShell commands only work on Windows")
}
// TODO: Implement actual PowerShell execution
// In production:
// cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command", psCommand)
// output, err := cmd.CombinedOutput()
// return string(output), err
c.logger.Debug("executing PowerShell command", "command", psCommand)
return "", nil
}