Compare commits
11 Commits
3a7fb4b223
...
fd185ed9b1
| Author | SHA1 | Date |
|---|---|---|
|
|
fd185ed9b1 | |
|
|
cb13bc9cfd | |
|
|
9047414ecd | |
|
|
a6cc10a689 | |
|
|
799a614e89 | |
|
|
356c6fbb49 | |
|
|
86eef472a9 | |
|
|
dbe9a06247 | |
|
|
ed39646ad2 | |
|
|
25183ea5cf | |
|
|
ce4fced380 |
|
|
@ -71,23 +71,62 @@ Eliminated hardcoded Icecast admin password from codebase.
|
||||||
- ~*supported-formats*~ → ~(config-supported-formats *config*)~
|
- ~*supported-formats*~ → ~(config-supported-formats *config*)~
|
||||||
- ~*stream-base-url*~ → ~(config-stream-base-url *config*)~
|
- ~*stream-base-url*~ → ~(config-stream-base-url *config*)~
|
||||||
|
|
||||||
|
** Template Security Fix (~template/login.ctml~) - CRITICAL
|
||||||
|
|
||||||
|
Removed hardcoded admin credentials display from login page:
|
||||||
|
|
||||||
|
- Deleted panel showing "Default Admin Credentials"
|
||||||
|
- No longer displays username: ~admin~ / password: ~asteroid123~
|
||||||
|
- Login page is now production-safe
|
||||||
|
|
||||||
|
This was the critical issue Fade mentioned: "the templates with the default passwords for sure need changing"
|
||||||
|
|
||||||
|
** Docker Security Fixes (~docker/docker-compose.yml~) - CRITICAL
|
||||||
|
|
||||||
|
*** Port Bindings Secured
|
||||||
|
|
||||||
|
All services now bind to localhost only (127.0.0.1) instead of 0.0.0.0:
|
||||||
|
|
||||||
|
- *Icecast*: ~127.0.0.1:8000:8000~ - Use HAproxy to expose publicly
|
||||||
|
- *Liquidsoap telnet*: ~127.0.0.1:1234:1234~ - Never expose to internet
|
||||||
|
- *PostgreSQL*: ~127.0.0.1:5432:5432~ - Database access from host only
|
||||||
|
|
||||||
|
This prevents external access to:
|
||||||
|
- Liquidsoap telnet management interface (Problem 1)
|
||||||
|
- Direct Icecast access without HAproxy (Problem 2)
|
||||||
|
- PostgreSQL database port
|
||||||
|
|
||||||
|
*** Passwords Externalized
|
||||||
|
|
||||||
|
All Docker container passwords now use environment variables:
|
||||||
|
|
||||||
|
#+BEGIN_SRC yaml
|
||||||
|
environment:
|
||||||
|
- ICECAST_SOURCE_PASSWORD=${ICECAST_SOURCE_PASSWORD:-default}
|
||||||
|
- ICECAST_ADMIN_PASSWORD=${ICECAST_ADMIN_PASSWORD:-default}
|
||||||
|
- ICECAST_RELAY_PASSWORD=${ICECAST_RELAY_PASSWORD:-default}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-default}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
* TODO Items Addressed
|
* TODO Items Addressed
|
||||||
|
|
||||||
** DONE Problem 4: Templates no longer advertise default admin password
|
** DONE Problem 4: Templates no longer advertise default admin password
|
||||||
** DONE Server runtime configuration: All configuration parameterized and loaded from environment
|
** DONE Server runtime configuration: All configuration parameterized and loaded from environment
|
||||||
|
** DONE Problem 1: Liquidsoap telnet binding secured (localhost only)
|
||||||
|
** DONE Problem 2: Icecast external binding secured (localhost only)
|
||||||
** TODO Problem 3: Database backend selection implemented (PostgreSQL support ready, migration needed)
|
** TODO Problem 3: Database backend selection implemented (PostgreSQL support ready, migration needed)
|
||||||
|
|
||||||
* Production Deployment Issues (From b612.asteroid.radio Test)
|
* Production Deployment Issues (From b612.asteroid.radio Test)
|
||||||
|
|
||||||
** Critical Security (Must fix before public launch)
|
** Critical Security (Must fix before public launch)
|
||||||
|
|
||||||
- [ ] *Problem 1*: Fix Liquidsoap telnet binding (currently exposed on external interface)
|
- [X] *Problem 1*: Fix Liquidsoap telnet binding ✅ FIXED
|
||||||
- Issue: ~telnet asteroid.radio 1234~ works from anywhere
|
- Issue: ~telnet asteroid.radio 1234~ works from anywhere
|
||||||
- Fix: Bind to localhost only in Docker config
|
- Fix: Bind to ~127.0.0.1:1234:1234~ in docker-compose.yml
|
||||||
|
|
||||||
- [ ] *Problem 2*: Fix Icecast external binding
|
- [X] *Problem 2*: Fix Icecast external binding ✅ FIXED
|
||||||
- Issue: Icecast binding to 0.0.0.0
|
- Issue: Icecast binding to 0.0.0.0
|
||||||
- Fix: Bind to localhost only, use HAproxy to proxy
|
- Fix: Bind to ~127.0.0.1:8000:8000~, use HAproxy to proxy
|
||||||
|
|
||||||
- [ ] *Problem 5*: Set up TLS/Let's Encrypt with HAproxy
|
- [ ] *Problem 5*: Set up TLS/Let's Encrypt with HAproxy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,59 @@
|
||||||
(api-output `(("status" . "error")
|
(api-output `(("status" . "error")
|
||||||
("message" . ,(format nil "Error creating user: ~a" e)))
|
("message" . ,(format nil "Error creating user: ~a" e)))
|
||||||
:status 500))))
|
:status 500))))
|
||||||
|
|
||||||
|
;; API: Change own password (authenticated users)
|
||||||
|
(define-api asteroid/user/change-password (current-password new-password) ()
|
||||||
|
"API endpoint for users to change their own password"
|
||||||
|
(require-authentication)
|
||||||
|
(handler-case
|
||||||
|
(if (and current-password new-password)
|
||||||
|
(let* ((user (get-current-user))
|
||||||
|
(username (gethash "username" user))
|
||||||
|
(stored-hash (gethash "password-hash" user)))
|
||||||
|
;; Verify current password
|
||||||
|
(if (verify-password current-password
|
||||||
|
(if (listp stored-hash) (first stored-hash) stored-hash))
|
||||||
|
;; Current password is correct, update to new password
|
||||||
|
(if (reset-user-password username new-password)
|
||||||
|
(api-output `(("status" . "success")
|
||||||
|
("message" . "Password changed successfully")))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Failed to update password"))
|
||||||
|
:status 500))
|
||||||
|
;; Current password is incorrect
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Current password is incorrect"))
|
||||||
|
:status 401)))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Missing required fields"))
|
||||||
|
:status 400))
|
||||||
|
(error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(format nil "Error changing password: ~a" e)))
|
||||||
|
:status 500))))
|
||||||
|
|
||||||
|
;; API: Reset user password (admin only)
|
||||||
|
(define-api asteroid/admin/reset-password (username new-password) ()
|
||||||
|
"API endpoint for admins to reset any user's password"
|
||||||
|
(require-role :admin)
|
||||||
|
(handler-case
|
||||||
|
(if (and username new-password)
|
||||||
|
(let ((user (find-user-by-username username)))
|
||||||
|
(if user
|
||||||
|
(if (reset-user-password username new-password)
|
||||||
|
(api-output `(("status" . "success")
|
||||||
|
("message" . ,(format nil "Password reset for user: ~a" username))))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Failed to reset password"))
|
||||||
|
:status 500))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(format nil "User not found: ~a" username)))
|
||||||
|
:status 404)))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "Missing required fields"))
|
||||||
|
:status 400))
|
||||||
|
(error (e)
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . ,(format nil "Error resetting password: ~a" e)))
|
||||||
|
:status 500))))
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ ASTEROID_STREAM_URL=http://localhost:8000
|
||||||
ICECAST_ADMIN_USER=admin
|
ICECAST_ADMIN_USER=admin
|
||||||
ICECAST_ADMIN_PASSWORD=CHANGE_THIS_PASSWORD
|
ICECAST_ADMIN_PASSWORD=CHANGE_THIS_PASSWORD
|
||||||
|
|
||||||
|
# Additional Icecast passwords (used by Docker containers)
|
||||||
|
# These are for Liquidsoap source connection and relay
|
||||||
|
ICECAST_SOURCE_PASSWORD=CHANGE_THIS_PASSWORD
|
||||||
|
ICECAST_RELAY_PASSWORD=CHANGE_THIS_PASSWORD
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATABASE CONFIGURATION
|
# DATABASE CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,16 @@ settings.frame.audio.channels.set(2)
|
||||||
settings.audio.converter.samplerate.libsamplerate.quality.set("best")
|
settings.audio.converter.samplerate.libsamplerate.quality.set("best")
|
||||||
|
|
||||||
# Enable telnet server for remote control
|
# Enable telnet server for remote control
|
||||||
|
# Bind to 0.0.0.0 inside container (Docker port mapping restricts external access)
|
||||||
|
# Security is enforced by docker-compose.yml: "127.0.0.1:1234:1234"
|
||||||
settings.server.telnet.set(true)
|
settings.server.telnet.set(true)
|
||||||
settings.server.telnet.port.set(1234)
|
settings.server.telnet.port.set(1234)
|
||||||
settings.server.telnet.bind_addr.set("0.0.0.0")
|
settings.server.telnet.bind_addr.set("0.0.0.0")
|
||||||
|
|
||||||
|
# Get Icecast source password from environment variable
|
||||||
|
# Falls back to default if not set (for development)
|
||||||
|
icecast_password = environment.get("ICECAST_SOURCE_PASSWORD", default="H1tn31EhsyLrfRmo")
|
||||||
|
|
||||||
# Create playlist source from generated M3U file
|
# Create playlist source from generated M3U file
|
||||||
# This file is managed by Asteroid's stream control system
|
# This file is managed by Asteroid's stream control system
|
||||||
# Falls back to directory scan if playlist file doesn't exist
|
# Falls back to directory scan if playlist file doesn't exist
|
||||||
|
|
@ -65,7 +71,7 @@ output.icecast(
|
||||||
%mp3(bitrate=128),
|
%mp3(bitrate=128),
|
||||||
host="icecast", # Docker service name
|
host="icecast", # Docker service name
|
||||||
port=8000,
|
port=8000,
|
||||||
password="H1tn31EhsyLrfRmo",
|
password=icecast_password,
|
||||||
mount="asteroid.mp3",
|
mount="asteroid.mp3",
|
||||||
name="Asteroid Radio",
|
name="Asteroid Radio",
|
||||||
description="Music for Hackers - Streaming from the Asteroid",
|
description="Music for Hackers - Streaming from the Asteroid",
|
||||||
|
|
@ -80,7 +86,7 @@ output.icecast(
|
||||||
%fdkaac(bitrate=96),
|
%fdkaac(bitrate=96),
|
||||||
host="icecast",
|
host="icecast",
|
||||||
port=8000,
|
port=8000,
|
||||||
password="H1tn31EhsyLrfRmo",
|
password=icecast_password,
|
||||||
mount="asteroid.aac",
|
mount="asteroid.aac",
|
||||||
name="Asteroid Radio (AAC)",
|
name="Asteroid Radio (AAC)",
|
||||||
description="Music for Hackers - High efficiency AAC stream",
|
description="Music for Hackers - High efficiency AAC stream",
|
||||||
|
|
@ -95,7 +101,7 @@ output.icecast(
|
||||||
%mp3(bitrate=64),
|
%mp3(bitrate=64),
|
||||||
host="icecast",
|
host="icecast",
|
||||||
port=8000,
|
port=8000,
|
||||||
password="H1tn31EhsyLrfRmo",
|
password=icecast_password,
|
||||||
mount="asteroid-low.mp3",
|
mount="asteroid-low.mp3",
|
||||||
name="Asteroid Radio (Low Quality)",
|
name="Asteroid Radio (Low Quality)",
|
||||||
description="Music for Hackers - Low bandwidth stream",
|
description="Music for Hackers - Low bandwidth stream",
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ services:
|
||||||
image: infiniteproject/icecast:latest
|
image: infiniteproject/icecast:latest
|
||||||
container_name: asteroid-icecast
|
container_name: asteroid-icecast
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "127.0.0.1:8000:8000" # Bind to localhost only - use HAproxy to expose publicly
|
||||||
volumes:
|
volumes:
|
||||||
- ./icecast.xml:/etc/icecast.xml
|
- ./icecast.xml:/etc/icecast.xml
|
||||||
environment:
|
environment:
|
||||||
- ICECAST_SOURCE_PASSWORD=H1tn31EhsyLrfRmo
|
- ICECAST_SOURCE_PASSWORD=${ICECAST_SOURCE_PASSWORD:-H1tn31EhsyLrfRmo}
|
||||||
- ICECAST_ADMIN_PASSWORD=asteroid_admin_2024
|
- ICECAST_ADMIN_PASSWORD=${ICECAST_ADMIN_PASSWORD:-asteroid_admin_2024}
|
||||||
- ICECAST_RELAY_PASSWORD=asteroid_relay_2024
|
- ICECAST_RELAY_PASSWORD=${ICECAST_RELAY_PASSWORD:-asteroid_relay_2024}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- asteroid-network
|
- asteroid-network
|
||||||
|
|
@ -20,7 +20,7 @@ services:
|
||||||
dockerfile: Dockerfile.liquidsoap
|
dockerfile: Dockerfile.liquidsoap
|
||||||
container_name: asteroid-liquidsoap
|
container_name: asteroid-liquidsoap
|
||||||
ports:
|
ports:
|
||||||
- "1234:1234"
|
- "127.0.0.1:1234:1234" # Bind telnet to localhost only - SECURITY: Never expose to internet
|
||||||
depends_on:
|
depends_on:
|
||||||
- icecast
|
- icecast
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -35,11 +35,11 @@ services:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: asteroid-postgres
|
container_name: asteroid-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: asteroid
|
POSTGRES_DB: ${POSTGRES_DB:-asteroid}
|
||||||
POSTGRES_USER: asteroid
|
POSTGRES_USER: ${POSTGRES_USER:-asteroid}
|
||||||
POSTGRES_PASSWORD: asteroid_db_2025
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-asteroid_db_2025}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "127.0.0.1:5432:5432" # Bind to localhost only
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres-data:/var/lib/postgresql/data
|
||||||
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@
|
||||||
</limits>
|
</limits>
|
||||||
|
|
||||||
<authentication>
|
<authentication>
|
||||||
|
<!-- NOTE: These passwords are OVERRIDDEN by environment variables in docker-compose.yml
|
||||||
|
Set ICECAST_SOURCE_PASSWORD, ICECAST_ADMIN_PASSWORD, and ICECAST_RELAY_PASSWORD
|
||||||
|
in your environment or .env file for production deployments.
|
||||||
|
These defaults are only used if environment variables are not set. -->
|
||||||
<source-password>H1tn31EhsyLrfRmo</source-password>
|
<source-password>H1tn31EhsyLrfRmo</source-password>
|
||||||
<relay-password>asteroid_relay_2024</relay-password>
|
<relay-password>asteroid_relay_2024</relay-password>
|
||||||
<admin-user>admin</admin-user>
|
<admin-user>admin</admin-user>
|
||||||
|
|
|
||||||
|
|
@ -658,3 +658,70 @@ async function updateLiveStreamInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password change functionality
|
||||||
|
function changeAdminPassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const currentPassword = document.getElementById('current-password').value;
|
||||||
|
const newPassword = document.getElementById('new-password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm-password').value;
|
||||||
|
const messageEl = document.getElementById('password-message');
|
||||||
|
|
||||||
|
// Clear previous messages
|
||||||
|
messageEl.style.display = 'none';
|
||||||
|
messageEl.className = 'message';
|
||||||
|
|
||||||
|
// Validate passwords match
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
showPasswordMessage('New passwords do not match', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password length
|
||||||
|
if (newPassword.length < 8) {
|
||||||
|
showPasswordMessage('Password must be at least 8 characters', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit password change
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append('current-password', currentPassword);
|
||||||
|
formData.append('new-password', newPassword);
|
||||||
|
|
||||||
|
fetch('/api/asteroid/user/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showPasswordMessage('Password changed successfully! Please use your new password on next login.', 'success');
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('change-password-form').reset();
|
||||||
|
} else {
|
||||||
|
showPasswordMessage(data.message || 'Failed to change password', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error changing password:', error);
|
||||||
|
showPasswordMessage('Error changing password. Please try again.', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPasswordMessage(message, type) {
|
||||||
|
const messageEl = document.getElementById('password-message');
|
||||||
|
messageEl.textContent = message;
|
||||||
|
messageEl.className = 'message ' + type;
|
||||||
|
messageEl.style.display = 'block';
|
||||||
|
|
||||||
|
// Auto-hide success messages after 5 seconds
|
||||||
|
if (type === 'success') {
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Account Security -->
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>🔒 Account Security</h2>
|
||||||
|
<div class="password-change-form">
|
||||||
|
<h3>Change Your Password</h3>
|
||||||
|
<div id="password-message" class="message" style="display: none;"></div>
|
||||||
|
<form id="change-password-form" onsubmit="changeAdminPassword(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current-password">Current Password:</label>
|
||||||
|
<input type="password" id="current-password" name="current-password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-password">New Password:</label>
|
||||||
|
<input type="password" id="new-password" name="new-password" required minlength="8">
|
||||||
|
<small>Minimum 8 characters</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm-password">Confirm New Password:</label>
|
||||||
|
<input type="password" id="confirm-password" name="confirm-password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Music Library Management -->
|
<!-- Music Library Management -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>Music Library Management</h2>
|
<h2>Music Library Management</h2>
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,6 @@
|
||||||
<button type="submit" class="btn btn-primary" style="width: 100%;">LOGIN</button>
|
<button type="submit" class="btn btn-primary" style="width: 100%;">LOGIN</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="panel" style="margin-top: 20px; text-align: center;">
|
|
||||||
<strong style="color: #ff6600;">Default Admin Credentials:</strong><br>
|
|
||||||
Username: <br><code style="color: #00ff00;">admin</code><br>
|
|
||||||
Password: <br><code style="color: #00ff00;">asteroid123</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue