diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 17d7209..04f449e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -365,6 +365,93 @@ curl http://minio.kindred.internal:9000/minio/health/live --- +## SSL/TLS with FreeIPA and Nginx + +For production deployments, Silo should be served over HTTPS using nginx as a reverse proxy with certificates from FreeIPA. + +### Setup IPA and Nginx + +Run the IPA/nginx setup script after the basic host setup: + +```bash +sudo /opt/silo/src/scripts/setup-ipa-nginx.sh +``` + +This script: +1. Installs FreeIPA client and nginx +2. Enrolls the host in FreeIPA domain +3. Requests SSL certificate from IPA CA (auto-renewed by certmonger) +4. Configures nginx as reverse proxy (HTTP → HTTPS redirect) +5. Opens firewall ports 80 and 443 + +### Manual Steps After Script + +1. Verify certificate was issued: + ```bash + getcert list + ``` + +2. The silo config is already updated to use `https://silo.kindred.internal` as base URL. Restart silo: + ```bash + sudo systemctl restart silod + ``` + +3. Test the setup: + ```bash + curl https://silo.kindred.internal/health + ``` + +### Certificate Management + +Certificates are automatically renewed by certmonger. Check status: + +```bash +# List all tracked certificates +getcert list + +# Check specific certificate +getcert list -f /etc/ssl/silo/silo.crt + +# Manual renewal if needed +getcert resubmit -f /etc/ssl/silo/silo.crt +``` + +### Trusting IPA CA on Clients + +For clients to trust the Silo HTTPS certificate, they need the IPA CA: + +```bash +# Download CA cert +curl -o /tmp/ipa-ca.crt https://ipa.kindred.internal/ipa/config/ca.crt + +# Ubuntu/Debian +sudo cp /tmp/ipa-ca.crt /usr/local/share/ca-certificates/ipa-ca.crt +sudo update-ca-certificates + +# RHEL/Fedora +sudo cp /tmp/ipa-ca.crt /etc/pki/ca-trust/source/anchors/ +sudo update-ca-trust +``` + +### Nginx Configuration + +The nginx config is installed at `/etc/nginx/sites-available/silo`. Key settings: + +- HTTP redirects to HTTPS +- TLS 1.2/1.3 only with strong ciphers +- Proxies to `127.0.0.1:8080` (silod) +- 100MB max upload size for CAD files +- Security headers (X-Frame-Options, etc.) + +To modify: +```bash +sudo nano /etc/nginx/sites-available/silo +sudo nginx -t +sudo systemctl reload nginx +``` + +--- + ## Security Checklist - [ ] `/etc/silo/silod.env` has mode 600 (`chmod 600`) @@ -372,5 +459,8 @@ curl http://minio.kindred.internal:9000/minio/health/live - [ ] MinIO credentials are specific to silo (not admin) - [ ] SSL/TLS enabled for PostgreSQL (`sslmode: require`) - [ ] SSL/TLS enabled for MinIO (`use_ssl: true`) if available -- [ ] Firewall restricts access to port 8080 +- [ ] HTTPS enabled via nginx reverse proxy +- [ ] Silod listens on localhost only (`host: 127.0.0.1`) +- [ ] Firewall allows only ports 80, 443 (not 8080) - [ ] Service runs as non-root `silo` user +- [ ] Host enrolled in FreeIPA for centralized auth (future) diff --git a/internal/api/csv.go b/internal/api/csv.go index 7208483..ffd1299 100644 --- a/internal/api/csv.go +++ b/internal/api/csv.go @@ -195,6 +195,7 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) { // Get options dryRun := r.FormValue("dry_run") == "true" + skipExisting := r.FormValue("skip_existing") == "true" schemaName := r.FormValue("schema") if schemaName == "" { schemaName = "kindred-rd" @@ -297,6 +298,10 @@ func (s *Server) HandleImportCSV(w http.ResponseWriter, r *http.Request) { if partNumber != "" { existing, _ := s.items.GetByPartNumber(ctx, partNumber) if existing != nil { + if skipExisting { + // Silently skip existing items + continue + } result.Errors = append(result.Errors, CSVImportErr{ Row: rowNum, Field: "part_number", @@ -532,6 +537,8 @@ func isStandardColumn(col string) bool { "updated_at": true, "category": true, "projects": true, + "objects": true, // FreeCAD objects data - skip on import + "archived_at": true, } return standardCols[col] } diff --git a/internal/api/templates/items.html b/internal/api/templates/items.html index 661693b..603eee2 100644 --- a/internal/api/templates/items.html +++ b/internal/api/templates/items.html @@ -444,6 +444,15 @@ Dry run (validate without creating items) +
Server returned non-JSON response (status ${response.status}). Check server logs.
${text.substring(0, 500)}`;
+ resultsDiv.className = "import-results error";
+ resultsDiv.style.display = "block";
+ return;
+ }
+
const result = await response.json();
if (!response.ok) {